Skip to content

Commit cf5e66c

Browse files
committed
Cache validation and composition as much as possible
1 parent f003e71 commit cf5e66c

File tree

1 file changed

+145
-63
lines changed

1 file changed

+145
-63
lines changed

packages/jupyterlab-lsp/src/settings.ts

Lines changed: 145 additions & 63 deletions
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,12 @@ function isJSONProperty(obj: unknown): obj is IJSONProperty {
4141
);
4242
}
4343

44+
/**
45+
* Settings keyed by language server name with values including
46+
* multiple properties, such as priority or workspace configuration
47+
*/
48+
type LanguageServerSettings = Record<string, ServerSchemaWrapper>;
49+
4450
/**
4551
* Get default values from JSON Schema properties field.
4652
*/
@@ -62,7 +68,26 @@ function getDefaults(
6268
return defaults;
6369
}
6470

65-
let validationAttempt = 0;
71+
/**
72+
* Schema and user data that for validation
73+
*/
74+
interface IValidationData {
75+
rawUserSettings: string;
76+
schema: ISettingRegistry.ISchema;
77+
}
78+
79+
/**
80+
* Conflicts encounteredn when dot-collapsing settings
81+
* organised by server ID, and then as a mapping between
82+
* (dotted) setting ID and list of encoutnered values.
83+
* The last encountered values is preferred for use.
84+
*/
85+
type SettingsMergeConflicts = Record<string, Record<string, any[]>>;
86+
87+
interface ISettingsCollapseResult {
88+
settings: LanguageServerSettings;
89+
conflicts: SettingsMergeConflicts;
90+
}
6691

6792
export class SettingsUIManager {
6893
constructor(
@@ -91,6 +116,10 @@ export class SettingsUIManager {
91116
}
92117
);
93118
}
119+
this._validationAttempt = 0;
120+
this._lastValidation = null;
121+
this._lastUserServerSettings = null;
122+
this._lastUserServerSettingsDoted = null;
94123
}
95124

96125
protected get console(): ILSPLogConsole {
@@ -205,43 +234,56 @@ export class SettingsUIManager {
205234
schema.properties!.language_servers.properties = knownServersConfig;
206235
schema.properties!.language_servers.default = defaults;
207236

208-
// test if we can apply the schema without causing validation error
209-
// (is the configuration held by the user compatible with the schema?)
210-
validationAttempt += 1;
211-
// the validator will parse raw plugin data into this object;
212-
// we do not do anything with those right now.
213-
const parsedData = { composite: {}, user: {} };
214-
const validationErrors =
215-
this.options.settingRegistry.validator.validateData(
216-
{
217-
// the plugin schema is cached so we have to provide a dummy ID.
218-
id: `lsp-validation-attempt-${validationAttempt}`,
219-
raw: plugin.raw,
220-
data: parsedData,
221-
version: plugin.version,
222-
schema: schema
223-
},
224-
true
225-
);
226-
227-
if (validationErrors) {
228-
console.error(
229-
'LSP server settings validation failed; configuration graphical interface will run in schema-free mode; errors:',
230-
validationErrors
231-
);
232-
this._validationErrors = validationErrors;
233-
if (!original) {
237+
const lastValidation = this._lastValidation;
238+
// do not re-validate if neither schema, nor user settings changed
239+
if (
240+
lastValidation === null ||
241+
lastValidation.rawUserSettings !== plugin.raw ||
242+
!JSONExt.deepEqual(lastValidation.schema, schema)
243+
) {
244+
// test if we can apply the schema without causing validation error
245+
// (is the configuration held by the user compatible with the schema?)
246+
this._validationAttempt += 1;
247+
// the validator will parse raw plugin data into this object;
248+
// we do not do anything with those right now.
249+
const parsedData = { composite: {}, user: {} };
250+
const validationErrors =
251+
this.options.settingRegistry.validator.validateData(
252+
{
253+
// the plugin schema is cached so we have to provide a dummy ID.
254+
id: `lsp-validation-attempt-${this._validationAttempt}`,
255+
raw: plugin.raw,
256+
data: parsedData,
257+
version: plugin.version,
258+
schema: schema
259+
},
260+
true
261+
);
262+
263+
if (validationErrors) {
234264
console.error(
235-
'Original language servers schema not available to restore non-transformed values.'
265+
'LSP server settings validation failed; graphical interface for settings will run in schema-free mode; errors:',
266+
validationErrors
236267
);
237-
} else {
238-
if (!original.properties!.language_servers.properties) {
239-
delete schema.properties!.language_servers.properties;
240-
}
241-
if (!original.properties!.language_servers.default) {
242-
delete schema.properties!.language_servers.default;
268+
this._validationErrors = validationErrors;
269+
if (!original) {
270+
console.error(
271+
'Original language servers schema not available to restore non-transformed values.'
272+
);
273+
} else {
274+
if (!original.properties!.language_servers.properties) {
275+
delete schema.properties!.language_servers.properties;
276+
}
277+
if (!original.properties!.language_servers.default) {
278+
delete schema.properties!.language_servers.default;
279+
}
243280
}
244281
}
282+
283+
this._lastValidation = {
284+
rawUserSettings: plugin.raw,
285+
schema: schema
286+
};
245287
}
246288

247289
this._defaults = defaults;
@@ -250,6 +292,15 @@ export class SettingsUIManager {
250292
// Transform the plugin object to return different schema than the default.
251293
this.options.settingRegistry.transform(pluginId, {
252294
fetch: plugin => {
295+
// Profiling data:
296+
// Initial fetch: 61-64 ms
297+
// Subsequent without change: <1ms
298+
// Session change: 642 ms.
299+
// 91% spent on `validateData()`
300+
// 10% in addSchema().
301+
// 1.8% spent on `deepCopy()`
302+
// 1.79% spend on other tasks in `populate()`
303+
253304
if (!original) {
254305
original = JSONExt.deepCopy(plugin.schema);
255306
}
@@ -258,6 +309,7 @@ export class SettingsUIManager {
258309
canonical = JSONExt.deepCopy(plugin.schema);
259310
populate(plugin, canonical);
260311
}
312+
261313
return {
262314
data: plugin.data,
263315
id: plugin.id,
@@ -267,12 +319,39 @@ export class SettingsUIManager {
267319
};
268320
},
269321
compose: plugin => {
322+
// Initial compose: 28 ms
323+
// Consecutive compose with cached settings: 1-2ms
324+
270325
const user = plugin.data.user as Required<LanguageServer>;
271326
const composite = JSONExt.deepCopy(user);
272327

273-
user.language_servers = this._collapseServerSettingsDotted(
274-
user.language_servers
275-
);
328+
// Cache collapsed settings for speed and to only show dialog once.
329+
// Note that JupyterLab attempts to transform in "preload" step (before splash screen end)
330+
// and then again for deferred extensions if the initial transform in preload timed out.
331+
// We are hitting the timeout in preload step.
332+
if (
333+
this._lastUserServerSettings === null ||
334+
this._lastUserServerSettingsDoted === null ||
335+
!JSONExt.deepEqual(
336+
this._lastUserServerSettings,
337+
user.language_servers
338+
)
339+
) {
340+
this._lastUserServerSettings = user.language_servers;
341+
const collapsed = this._collapseServerSettingsDotted(
342+
user.language_servers
343+
);
344+
user.language_servers = this._collapseServerSettingsDotted(
345+
user.language_servers
346+
).settings;
347+
this._lastUserServerSettingsDoted = user.language_servers;
348+
349+
if (Object.keys(collapsed.conflicts).length > 0) {
350+
this._warnConflicts(collapsed.conflicts).catch(this.console.warn);
351+
}
352+
} else {
353+
user.language_servers = this._lastUserServerSettingsDoted;
354+
}
276355
composite.language_servers = user.language_servers;
277356

278357
// Currently disabled, as it does not provide an obvious benefit:
@@ -288,6 +367,7 @@ export class SettingsUIManager {
288367
user: user,
289368
composite: composite
290369
};
370+
291371
return plugin;
292372
}
293373
});
@@ -300,14 +380,21 @@ export class SettingsUIManager {
300380
});
301381
}
302382

303-
protected _collapseServerSettingsDotted(
304-
settings: Record<string, ServerSchemaWrapper | null>
305-
): ServerSchemaWrapper {
383+
private async _warnConflicts(conflicts: SettingsMergeConflicts) {
384+
showDialog({
385+
body: renderCollapseConflicts({
386+
conflicts: conflicts,
387+
trans: this.options.trans
388+
}),
389+
buttons: [Dialog.okButton()]
390+
}).catch(console.warn);
391+
}
392+
393+
private _collapseServerSettingsDotted(
394+
settings: LanguageServerSettings
395+
): ISettingsCollapseResult {
306396
const conflicts: Record<string, Record<string, any[]>> = {};
307-
const result = JSONExt.deepCopy(settings) as Record<
308-
string,
309-
ServerSchemaWrapper
310-
>;
397+
const result = JSONExt.deepCopy(settings) as LanguageServerSettings;
311398
for (let [serverKey, serverSettingsGroup] of Object.entries(settings)) {
312399
if (!serverSettingsGroup || !serverSettingsGroup.serverSettings) {
313400
continue;
@@ -316,27 +403,18 @@ export class SettingsUIManager {
316403
serverSettingsGroup.serverSettings as ReadonlyJSONObject
317404
);
318405
conflicts[serverKey] = collapsed.conflicts;
319-
result[serverKey].serverSettings = collapsed.result;
320-
}
321-
if (Object.keys(conflicts).length > 0) {
322-
showDialog({
323-
body: renderCollapseConflicts({
324-
conflicts: conflicts,
325-
trans: this.options.trans
326-
}),
327-
buttons: [Dialog.okButton()]
328-
}).catch(console.warn);
406+
result[serverKey]!.serverSettings = collapsed.result;
329407
}
330-
return result;
408+
return {
409+
settings: result,
410+
conflicts: conflicts
411+
};
331412
}
332413

333414
protected _filterOutDefaults(
334-
settings: Record<string, ServerSchemaWrapper | null>
335-
): ServerSchemaWrapper {
336-
const result = JSONExt.deepCopy(settings) as Record<
337-
string,
338-
ServerSchemaWrapper
339-
>;
415+
settings: LanguageServerSettings
416+
): LanguageServerSettings {
417+
const result = JSONExt.deepCopy(settings) as LanguageServerSettings;
340418
for (let [serverKey, serverSettingsGroup] of Object.entries(result)) {
341419
const serverDefaults = this._defaults[serverKey];
342420
if (serverDefaults == null) {
@@ -357,12 +435,12 @@ export class SettingsUIManager {
357435
settingValue as ReadonlyJSONObject
358436
)) {
359437
if (JSONExt.deepEqual(subValue as any, settingDefault[subKey])) {
360-
delete result[serverKey][settingKey]![subKey];
438+
delete result[serverKey]![settingKey]![subKey];
361439
}
362440
}
363441
} else {
364442
if (JSONExt.deepEqual(settingValue as any, settingDefault)) {
365-
delete result[serverKey][settingKey];
443+
delete result[serverKey]![settingKey];
366444
}
367445
}
368446
}
@@ -372,4 +450,8 @@ export class SettingsUIManager {
372450

373451
private _defaults: Record<string, any>;
374452
private _validationErrors: ISchemaValidator.IError[];
453+
private _validationAttempt: number;
454+
private _lastValidation: IValidationData | null;
455+
private _lastUserServerSettings: LanguageServerSettings | null;
456+
private _lastUserServerSettingsDoted: LanguageServerSettings | null;
375457
}

0 commit comments

Comments
 (0)