Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fixed slow startup problems #1370

Merged
merged 1 commit into from
Dec 5, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions lib/papi-dts/papi.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4050,6 +4050,8 @@ declare module 'shared/models/project-lookup.service-model' {
/** Local object of functions to run locally on each process as part of the project lookup service */
export const projectLookupServiceBase: ProjectLookupServiceType;
/**
* Gets project metadata from PDPFs filtered down by various filtering options
*
* Note: If there are multiple PDPs available whose metadata matches the conditions provided by the
* parameters, their project metadata will all be combined, so all available `projectInterface`s
* provided by the PDP Factory with the matching id (or all PDP Factories if no id is specified) for
Expand Down Expand Up @@ -4111,6 +4113,7 @@ declare module 'shared/models/project-lookup.service-model' {
internalGetMetadata: typeof internalGetMetadata;
compareProjectDataProviderFactoryMetadataInfoMinimalMatch: typeof compareProjectDataProviderFactoryMetadataInfoMinimalMatch;
transformGetMetadataForProjectParametersToFilter: typeof transformGetMetadataForProjectParametersToFilter;
LOAD_TIME_GRACE_PERIOD_MS: number;
};
}
declare module 'shared/services/project-lookup.service' {
Expand Down
217 changes: 217 additions & 0 deletions src/extension-host/services/contribution.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,217 @@
import { ExtensionManifest } from '@extension-host/extension-types/extension-manifest.model';
import { AsyncVariable, JsonDocumentLike, Unsubscriber } from 'platform-bible-utils';
import * as nodeFS from '@node/services/node-file-system.service';
import { joinUriPaths } from '@node/utils/util';
import logger from '@shared/services/logger.service';
import SettingsDocumentCombiner from '@shared/utils/settings-document-combiner';
import { platformSettings } from '@extension-host/data/core-settings-info.data';
import MenuDocumentCombiner from '@shared/utils/menu-document-combiner';
import menuDataObject from '@extension-host/data/menu.data.json';
import ProjectSettingsDocumentCombiner from '@shared/utils/project-settings-document-combiner';
import { platformProjectSettings } from '@extension-host/data/core-project-settings-info.data';
import LocalizedStringsDocumentCombiner from '@shared/utils/localized-strings-document-combiner';

// #region document combiners - manage the extension contributions. Services should layer over these

/**
* Object that keeps track of all settings contributions in the platform. To listen to updates to
* the settings contributions, subscribe to its `onDidRebuild` event (consider debouncing as each
* contribution will trigger a rebuild).
*
* Keeping this object separate from the data provider and disabling the `set` calls in the data
* provider prevents random services from changing system settings contributions unexpectedly.
*/
export const settingsDocumentCombiner = new SettingsDocumentCombiner(platformSettings);
/**
* Object that keeps track of all active menus in the platform. Call
* {@link MenuDataDataProviderEngine.rebuildMenus} in the service host after updating this object.
*
* Keeping this object separate from the data provider and disabling the `set` calls in the data
* provider prevents random services from changing system menus unexpectedly.
*/
export const menuDocumentCombiner = new MenuDocumentCombiner(menuDataObject);
/**
* Object that keeps track of all project settings contributions in the platform. To listen to
* updates to the project settings contributions, subscribe to its `onDidRebuild` event (consider
* debouncing as each contribution will trigger a rebuild).
*
* Keeping this object separate from the network object prevents random services from changing
* system project settings contributions unexpectedly.
*/
export const projectSettingsDocumentCombiner = new ProjectSettingsDocumentCombiner(
platformProjectSettings,
);
/**
* Object that keeps track of all localized string contributions in the platform. To listen to
* updates to the localized string contributions, subscribe to its `onDidRebuild` event (consider
* debouncing as each contribution will trigger a rebuild).
*
* Keeping this object separate from the data provider and disabling the `set` calls in the data
* provider prevents random services from changing system localized string contributions
* unexpectedly.
*/
export const localizedStringsDocumentCombiner = new LocalizedStringsDocumentCombiner({});

// #endregion

const onDidResyncContributionsSubscriptions: (() => Promise<void>)[] = [];
/**
* Event that runs asynchronous functions after extension contributions are finished being set up
* and after they are re-synced. Function calls are awaited
*/
export function onDidResyncContributions(callback: () => Promise<void>): Unsubscriber {
onDidResyncContributionsSubscriptions.push(callback);
return () => {
const callbackIndex = onDidResyncContributionsSubscriptions.indexOf(callback);
if (callbackIndex < 0) return false;
onDidResyncContributionsSubscriptions.splice(callbackIndex, 1);
return true;
};
}

let resyncContributionsCount = -1;
function createNewResyncContributionsVariable() {
resyncContributionsCount += 1;
return new AsyncVariable<undefined>(`resyncContributions ${resyncContributionsCount}`, -1);
}
let resyncContributionsVariable = createNewResyncContributionsVariable();

/**
* Returns a promise that resolves when the extension service is done resyncing contributions. If it
* has already set up contributions before and is not currently resyncing contributions, the promise
* will already be resolved.
*
* This should be used when attempting to access contribution information to ensure you aren't
* accessing incomplete or out-of-date contribution information
*/
export async function waitForResyncContributions() {
return resyncContributionsVariable.promise;
}

/** Clears extension contributions and re-adds them. Only call from `extension.service.ts`! */
export async function resyncContributions(
extensionsToAdd: Readonly<ExtensionManifest & { dirUri: string }>[],
) {
if (resyncContributionsVariable.hasSettled)
resyncContributionsVariable = createNewResyncContributionsVariable();

menuDocumentCombiner.deleteAllContributions();
settingsDocumentCombiner.deleteAllContributions();
projectSettingsDocumentCombiner.deleteAllContributions();
localizedStringsDocumentCombiner.deleteAllContributions();

// Load up all the extension contributions asynchronously
const extensionsContributions = await Promise.all(
extensionsToAdd.map(async (extension) => {
let localizedStringsDocument: JsonDocumentLike | undefined;
if (extension.localizedStrings) {
try {
// TODO: Provide a way to make sure extensions don't tell us to read files outside their dir
const localizedStringsJson = await nodeFS.readFileText(
joinUriPaths(extension.dirUri, extension.localizedStrings),
);
localizedStringsDocument = JSON.parse(localizedStringsJson);
} catch (error) {
logger.warn(
`Could not load localized strings contribution for ${extension.name}: ${error}`,
);
}
}
let menuDocument: JsonDocumentLike | undefined;
if (extension.menus) {
try {
// TODO: Provide a way to make sure extensions don't tell us to read files outside their dir
const menuJson = await nodeFS.readFileText(
joinUriPaths(extension.dirUri, extension.menus),
);
menuDocument = JSON.parse(menuJson);
} catch (error) {
logger.warn(`Could not load menu contribution for ${extension.name}: ${error}`);
}
}
let settingsDocument: JsonDocumentLike | undefined;
if (extension.settings) {
try {
// TODO: Provide a way to make sure extensions don't tell us to read files outside their dir
const settingsJson = await nodeFS.readFileText(
joinUriPaths(extension.dirUri, extension.settings),
);
settingsDocument = JSON.parse(settingsJson);
} catch (error) {
logger.warn(`Could not load settings contribution for ${extension.name}: ${error}`);
}
}
let projectSettingsDocument: JsonDocumentLike | undefined;
if (extension.projectSettings) {
try {
// TODO: Provide a way to make sure extensions don't tell us to read files outside their dir
const projectSettingsJson = await nodeFS.readFileText(
joinUriPaths(extension.dirUri, extension.projectSettings),
);
projectSettingsDocument = JSON.parse(projectSettingsJson);
} catch (error) {
logger.warn(
`Could not load project settings contribution for ${extension.name}: ${error}`,
);
}
}

return {
name: extension.name,
localizedStringsDocument,
menuDocument,
settingsDocument,
projectSettingsDocument,
};
}),
);

// Load contributions in the order in which the extensions are loaded
extensionsContributions.forEach(
({
name,
localizedStringsDocument,
menuDocument,
settingsDocument,
projectSettingsDocument,
}) => {
if (localizedStringsDocument)
try {
localizedStringsDocumentCombiner.addOrUpdateContribution(name, localizedStringsDocument);
} catch (error) {
logger.warn(`Could not add localized strings contribution for ${name}: ${error}`);
}
if (menuDocument)
try {
menuDocumentCombiner.addOrUpdateContribution(name, menuDocument);
} catch (error) {
logger.warn(`Could not add menu contribution for ${name}: ${error}`);
}
if (settingsDocument)
try {
settingsDocumentCombiner.addOrUpdateContribution(name, settingsDocument);
} catch (error) {
logger.warn(`Could not add settings contribution for ${name}: ${error}`);
}
if (projectSettingsDocument)
try {
projectSettingsDocumentCombiner.addOrUpdateContribution(name, projectSettingsDocument);
} catch (error) {
logger.warn(`Could not add project settings contribution for ${name}: ${error}`);
}
},
);

resyncContributionsVariable.resolveToValue(undefined, true);

// Do things in response to finishing resyncing contributions
await Promise.all(
[...onDidResyncContributionsSubscriptions].map(async (callback, i) => {
try {
await callback();
} catch (e) {
logger.warn(`onDidResyncContributionsSubscriptions threw on callback ${i}! ${e}`);
}
}),
);
}
120 changes: 1 addition & 119 deletions src/extension-host/services/extension.service.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,16 +27,10 @@ import {
stringLength,
startsWith,
slice,
JsonDocumentLike,
} from 'platform-bible-utils';
import LogError from '@shared/log-error.model';
import { ExtensionManifest } from '@extension-host/extension-types/extension-manifest.model';
import { menuDocumentCombiner } from '@extension-host/services/menu-data.service-host';
import menuDataService from '@shared/services/menu-data.service';
import { localizedStringsDocumentCombiner } from '@extension-host/services/localization.service-host';
import { settingsDocumentCombiner } from '@extension-host/services/settings.service-host';
import { PLATFORM_NAMESPACE } from '@shared/data/platform.data';
import { projectSettingsDocumentCombiner } from '@extension-host/services/project-settings.service-host';
import {
ElevatedPrivilegeNames,
ElevatedPrivileges,
Expand All @@ -51,6 +45,7 @@ import {
import { CreateProcess } from '@shared/models/create-process-privilege.model';
import { wrappedFork, wrappedSpawn } from '@extension-host/services/create-process.service';
import os from 'os';
import { resyncContributions } from '@extension-host/services/contribution.service';

/**
* The way to use `require` directly - provided by webpack because they overwrite normal `require`.
Expand Down Expand Up @@ -1035,119 +1030,6 @@ async function deactivateExtensions(extensions: ExtensionInfo[]): Promise<void>
}
}

async function resyncContributions(
extensionsToAdd: Readonly<ExtensionManifest & { dirUri: string }>[],
) {
menuDocumentCombiner.deleteAllContributions();
settingsDocumentCombiner.deleteAllContributions();
projectSettingsDocumentCombiner.deleteAllContributions();
localizedStringsDocumentCombiner.deleteAllContributions();

// Load up all the extension contributions asynchronously
const extensionsContributions = await Promise.all(
extensionsToAdd.map(async (extension) => {
let localizedStringsDocument: JsonDocumentLike | undefined;
if (extension.localizedStrings) {
try {
// TODO: Provide a way to make sure extensions don't tell us to read files outside their dir
const localizedStringsJson = await nodeFS.readFileText(
joinUriPaths(extension.dirUri, extension.localizedStrings),
);
localizedStringsDocument = JSON.parse(localizedStringsJson);
} catch (error) {
logger.warn(
`Could not load localized strings contribution for ${extension.name}: ${error}`,
);
}
}
let menuDocument: JsonDocumentLike | undefined;
if (extension.menus) {
try {
// TODO: Provide a way to make sure extensions don't tell us to read files outside their dir
const menuJson = await nodeFS.readFileText(
joinUriPaths(extension.dirUri, extension.menus),
);
menuDocument = JSON.parse(menuJson);
} catch (error) {
logger.warn(`Could not load menu contribution for ${extension.name}: ${error}`);
}
}
let settingsDocument: JsonDocumentLike | undefined;
if (extension.settings) {
try {
// TODO: Provide a way to make sure extensions don't tell us to read files outside their dir
const settingsJson = await nodeFS.readFileText(
joinUriPaths(extension.dirUri, extension.settings),
);
settingsDocument = JSON.parse(settingsJson);
} catch (error) {
logger.warn(`Could not load settings contribution for ${extension.name}: ${error}`);
}
}
let projectSettingsDocument: JsonDocumentLike | undefined;
if (extension.projectSettings) {
try {
// TODO: Provide a way to make sure extensions don't tell us to read files outside their dir
const projectSettingsJson = await nodeFS.readFileText(
joinUriPaths(extension.dirUri, extension.projectSettings),
);
projectSettingsDocument = JSON.parse(projectSettingsJson);
} catch (error) {
logger.warn(
`Could not load project settings contribution for ${extension.name}: ${error}`,
);
}
}

return {
name: extension.name,
localizedStringsDocument,
menuDocument,
settingsDocument,
projectSettingsDocument,
};
}),
);

// Load contributions in the order in which the extensions are loaded
extensionsContributions.forEach(
({
name,
localizedStringsDocument,
menuDocument,
settingsDocument,
projectSettingsDocument,
}) => {
if (localizedStringsDocument)
try {
localizedStringsDocumentCombiner.addOrUpdateContribution(name, localizedStringsDocument);
} catch (error) {
logger.warn(`Could not add localized strings contribution for ${name}: ${error}`);
}
if (menuDocument)
try {
menuDocumentCombiner.addOrUpdateContribution(name, menuDocument);
} catch (error) {
logger.warn(`Could not add menu contribution for ${name}: ${error}`);
}
if (settingsDocument)
try {
settingsDocumentCombiner.addOrUpdateContribution(name, settingsDocument);
} catch (error) {
logger.warn(`Could not add settings contribution for ${name}: ${error}`);
}
if (projectSettingsDocument)
try {
projectSettingsDocumentCombiner.addOrUpdateContribution(name, projectSettingsDocument);
} catch (error) {
logger.warn(`Could not add project settings contribution for ${name}: ${error}`);
}
},
);

await menuDataService.rebuildMenus();
}

async function reloadExtensions(
shouldDeactivateExtensions: boolean,
shouldEmitDidReloadEvent: boolean,
Expand Down
5 changes: 5 additions & 0 deletions src/extension-host/services/localization.service-host.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,11 @@ jest.mock('@shared/services/logger.service', () => ({
warn: jest.fn(() => {}),
},
}));
jest.mock('@extension-host/services/contribution.service', () => ({
...jest.requireActual('@extension-host/services/contribution.service'),
// Don't actually wait because we're not syncing any contributions in these tests
waitForResyncContributions: async () => {},
}));

let localizationDataProviderEngine: Awaited<
ReturnType<typeof testingLocalizationService.implementLocalizationDataProviderEngine>
Expand Down
Loading
Loading