mirror of
https://github.com/elastic/kibana.git
synced 2025-04-24 01:38:56 -04:00
status service: get rid of calculateDepthRecursive
(#179160)
## Summary
We were spending a lot of time in `calculateDepthRecursive`
<img width="1367" alt="Screenshot 2024-03-21 at 13 46 00"
src="11047c7b
-9373-4976-ba40-8e6ad8d15373">
And it could fully be avoided given the plugin service already sorts the
plugins.
This PR changes the way the plugin system exposes its
`PluginDependencies` so that the keys are topologically ordered (same
order we use to setup / start the plugins) and then use that ordering
directly from the status service.
This commit is contained in:
parent
7ca14caca7
commit
386c29094d
4 changed files with 115 additions and 129 deletions
|
@ -128,7 +128,7 @@ test('getPlugins returns the list of plugins', () => {
|
|||
expect(pluginsSystem.getPlugins()).toEqual([pluginA, pluginB]);
|
||||
});
|
||||
|
||||
test('getPluginDependencies returns dependency tree of symbols', () => {
|
||||
test('getPluginDependencies returns dependency tree with keys topologically sorted', () => {
|
||||
pluginsSystem.addPlugin(createPlugin('plugin-a', { required: ['no-dep'] }));
|
||||
pluginsSystem.addPlugin(
|
||||
createPlugin('plugin-b', { required: ['plugin-a'], optional: ['no-dep', 'other'] })
|
||||
|
@ -138,6 +138,7 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
|
|||
expect(pluginsSystem.getPluginDependencies()).toMatchInlineSnapshot(`
|
||||
Object {
|
||||
"asNames": Map {
|
||||
"no-dep" => Array [],
|
||||
"plugin-a" => Array [
|
||||
"no-dep",
|
||||
],
|
||||
|
@ -145,9 +146,9 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
|
|||
"plugin-a",
|
||||
"no-dep",
|
||||
],
|
||||
"no-dep" => Array [],
|
||||
},
|
||||
"asOpaqueIds": Map {
|
||||
Symbol(no-dep) => Array [],
|
||||
Symbol(plugin-a) => Array [
|
||||
Symbol(no-dep),
|
||||
],
|
||||
|
@ -155,7 +156,6 @@ test('getPluginDependencies returns dependency tree of symbols', () => {
|
|||
Symbol(plugin-a),
|
||||
Symbol(no-dep),
|
||||
],
|
||||
Symbol(no-dep) => Array [],
|
||||
},
|
||||
}
|
||||
`);
|
||||
|
|
|
@ -34,6 +34,7 @@ export class PluginsSystem<T extends PluginType> {
|
|||
private readonly log: Logger;
|
||||
// `satup`, the past-tense version of the noun `setup`.
|
||||
private readonly satupPlugins: PluginName[] = [];
|
||||
private sortedPluginNames?: Set<string>;
|
||||
|
||||
constructor(private readonly coreContext: CoreContext, public readonly type: T) {
|
||||
this.log = coreContext.logger.get('plugins-system', this.type);
|
||||
|
@ -47,6 +48,9 @@ export class PluginsSystem<T extends PluginType> {
|
|||
}
|
||||
|
||||
this.plugins.set(plugin.name, plugin);
|
||||
|
||||
// clear sorted plugin name cache on addition
|
||||
this.sortedPluginNames = undefined;
|
||||
}
|
||||
|
||||
public getPlugins() {
|
||||
|
@ -54,32 +58,31 @@ export class PluginsSystem<T extends PluginType> {
|
|||
}
|
||||
|
||||
/**
|
||||
* @returns a ReadonlyMap of each plugin and an Array of its available dependencies
|
||||
* @returns a Map of each plugin and an Array of its available dependencies
|
||||
* @internal
|
||||
*/
|
||||
public getPluginDependencies(): PluginDependencies {
|
||||
const asNames = new Map(
|
||||
[...this.plugins].map(([name, plugin]) => [
|
||||
const asNames = new Map<string, string[]>();
|
||||
const asOpaqueIds = new Map<symbol, symbol[]>();
|
||||
|
||||
for (const pluginName of this.getTopologicallySortedPluginNames()) {
|
||||
const plugin = this.plugins.get(pluginName)!;
|
||||
const dependencies = [
|
||||
...new Set([
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
|
||||
]),
|
||||
];
|
||||
|
||||
asNames.set(
|
||||
plugin.name,
|
||||
[
|
||||
...new Set([
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
|
||||
]),
|
||||
].map((depId) => this.plugins.get(depId)!.name),
|
||||
])
|
||||
);
|
||||
const asOpaqueIds = new Map(
|
||||
[...this.plugins].map(([name, plugin]) => [
|
||||
dependencies.map((depId) => this.plugins.get(depId)!.name)
|
||||
);
|
||||
asOpaqueIds.set(
|
||||
plugin.opaqueId,
|
||||
[
|
||||
...new Set([
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter((optPlugin) => this.plugins.has(optPlugin)),
|
||||
]),
|
||||
].map((depId) => this.plugins.get(depId)!.opaqueId),
|
||||
])
|
||||
);
|
||||
dependencies.map((depId) => this.plugins.get(depId)!.opaqueId)
|
||||
);
|
||||
}
|
||||
|
||||
return { asNames, asOpaqueIds };
|
||||
}
|
||||
|
@ -298,68 +301,75 @@ export class PluginsSystem<T extends PluginType> {
|
|||
return publicPlugins;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets topologically sorted plugin names that are registered with the plugin system.
|
||||
* Ordering is possible if and only if the plugins graph has no directed cycles,
|
||||
* that is, if it is a directed acyclic graph (DAG). If plugins cannot be ordered
|
||||
* an error is thrown.
|
||||
*
|
||||
* Uses Kahn's Algorithm to sort the graph.
|
||||
*/
|
||||
private getTopologicallySortedPluginNames() {
|
||||
// We clone plugins so we can remove handled nodes while we perform the
|
||||
// topological ordering. If the cloned graph is _not_ empty at the end, we
|
||||
// know we were not able to topologically order the graph. We exclude optional
|
||||
// dependencies that are not present in the plugins graph.
|
||||
const pluginsDependenciesGraph = new Map(
|
||||
[...this.plugins.entries()].map(([pluginName, plugin]) => {
|
||||
return [
|
||||
pluginName,
|
||||
new Set([
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter((dependency) => this.plugins.has(dependency)),
|
||||
]),
|
||||
] as [PluginName, Set<PluginName>];
|
||||
})
|
||||
);
|
||||
|
||||
// First, find a list of "start nodes" which have no outgoing edges. At least
|
||||
// one such node must exist in a non-empty acyclic graph.
|
||||
const pluginsWithAllDependenciesSorted = [...pluginsDependenciesGraph.keys()].filter(
|
||||
(pluginName) => pluginsDependenciesGraph.get(pluginName)!.size === 0
|
||||
);
|
||||
|
||||
const sortedPluginNames = new Set<PluginName>();
|
||||
while (pluginsWithAllDependenciesSorted.length > 0) {
|
||||
const sortedPluginName = pluginsWithAllDependenciesSorted.pop()!;
|
||||
|
||||
// We know this plugin has all its dependencies sorted, so we can remove it
|
||||
// and include into the final result.
|
||||
pluginsDependenciesGraph.delete(sortedPluginName);
|
||||
sortedPluginNames.add(sortedPluginName);
|
||||
|
||||
// Go through the rest of the plugins and remove `sortedPluginName` from their
|
||||
// unsorted dependencies.
|
||||
for (const [pluginName, dependencies] of pluginsDependenciesGraph) {
|
||||
// If we managed delete `sortedPluginName` from dependencies let's check
|
||||
// whether it was the last one and we can mark plugin as sorted.
|
||||
if (dependencies.delete(sortedPluginName) && dependencies.size === 0) {
|
||||
pluginsWithAllDependenciesSorted.push(pluginName);
|
||||
}
|
||||
}
|
||||
if (!this.sortedPluginNames) {
|
||||
this.sortedPluginNames = getTopologicallySortedPluginNames(this.plugins);
|
||||
}
|
||||
|
||||
if (pluginsDependenciesGraph.size > 0) {
|
||||
const edgesLeft = JSON.stringify([...pluginsDependenciesGraph.keys()]);
|
||||
throw new Error(
|
||||
`Topological ordering of plugins did not complete, these plugins have cyclic or missing dependencies: ${edgesLeft}`
|
||||
);
|
||||
}
|
||||
|
||||
return sortedPluginNames;
|
||||
return this.sortedPluginNames;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets topologically sorted plugin names that are registered with the plugin system.
|
||||
* Ordering is possible if and only if the plugins graph has no directed cycles,
|
||||
* that is, if it is a directed acyclic graph (DAG). If plugins cannot be ordered
|
||||
* an error is thrown.
|
||||
*
|
||||
* Uses Kahn's Algorithm to sort the graph.
|
||||
*/
|
||||
const getTopologicallySortedPluginNames = (plugins: Map<PluginName, PluginWrapper>) => {
|
||||
// We clone plugins so we can remove handled nodes while we perform the
|
||||
// topological ordering. If the cloned graph is _not_ empty at the end, we
|
||||
// know we were not able to topologically order the graph. We exclude optional
|
||||
// dependencies that are not present in the plugins graph.
|
||||
const pluginsDependenciesGraph = new Map(
|
||||
[...plugins.entries()].map(([pluginName, plugin]) => {
|
||||
return [
|
||||
pluginName,
|
||||
new Set([
|
||||
...plugin.requiredPlugins,
|
||||
...plugin.optionalPlugins.filter((dependency) => plugins.has(dependency)),
|
||||
]),
|
||||
] as [PluginName, Set<PluginName>];
|
||||
})
|
||||
);
|
||||
|
||||
// First, find a list of "start nodes" which have no outgoing edges. At least
|
||||
// one such node must exist in a non-empty acyclic graph.
|
||||
const pluginsWithAllDependenciesSorted = [...pluginsDependenciesGraph.keys()].filter(
|
||||
(pluginName) => pluginsDependenciesGraph.get(pluginName)!.size === 0
|
||||
);
|
||||
|
||||
const sortedPluginNames = new Set<PluginName>();
|
||||
while (pluginsWithAllDependenciesSorted.length > 0) {
|
||||
const sortedPluginName = pluginsWithAllDependenciesSorted.pop()!;
|
||||
|
||||
// We know this plugin has all its dependencies sorted, so we can remove it
|
||||
// and include into the final result.
|
||||
pluginsDependenciesGraph.delete(sortedPluginName);
|
||||
sortedPluginNames.add(sortedPluginName);
|
||||
|
||||
// Go through the rest of the plugins and remove `sortedPluginName` from their
|
||||
// unsorted dependencies.
|
||||
for (const [pluginName, dependencies] of pluginsDependenciesGraph) {
|
||||
// If we managed delete `sortedPluginName` from dependencies let's check
|
||||
// whether it was the last one and we can mark plugin as sorted.
|
||||
if (dependencies.delete(sortedPluginName) && dependencies.size === 0) {
|
||||
pluginsWithAllDependenciesSorted.push(pluginName);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (pluginsDependenciesGraph.size > 0) {
|
||||
const edgesLeft = JSON.stringify([...pluginsDependenciesGraph.keys()]);
|
||||
throw new Error(
|
||||
`Topological ordering of plugins did not complete, these plugins have cyclic or missing dependencies: ${edgesLeft}`
|
||||
);
|
||||
}
|
||||
|
||||
return sortedPluginNames;
|
||||
};
|
||||
|
||||
const buildReverseDependencyMap = (
|
||||
pluginMap: Map<PluginName, PluginWrapper>
|
||||
): Map<PluginName, PluginName[]> => {
|
||||
|
|
|
@ -10,6 +10,16 @@ import type { PluginName, PluginOpaqueId } from '@kbn/core-base-common';
|
|||
|
||||
/** @internal */
|
||||
export interface PluginDependencies {
|
||||
/**
|
||||
* Plugin to dependencies map with plugin names as key/values.
|
||||
*
|
||||
* Keys sorted by plugin topological order (root plugins first, leaf plugins last).
|
||||
*/
|
||||
asNames: ReadonlyMap<PluginName, PluginName[]>;
|
||||
/**
|
||||
* Plugin to dependencies map with plugin opaque ids as key/values.
|
||||
*
|
||||
* Keys sorted by plugin topological order (root plugins first, leaf plugins last).
|
||||
*/
|
||||
asOpaqueIds: ReadonlyMap<PluginOpaqueId, PluginOpaqueId[]>;
|
||||
}
|
||||
|
|
|
@ -23,7 +23,6 @@ import {
|
|||
takeUntil,
|
||||
delay,
|
||||
} from 'rxjs/operators';
|
||||
import { sortBy } from 'lodash';
|
||||
import { isDeepStrictEqual } from 'util';
|
||||
import type { PluginName } from '@kbn/core-base-common';
|
||||
import { ServiceStatusLevels, type CoreStatus, type ServiceStatus } from '@kbn/core-status-common';
|
||||
|
@ -45,7 +44,6 @@ export interface Deps {
|
|||
interface PluginData {
|
||||
[name: PluginName]: {
|
||||
name: PluginName;
|
||||
depth: number; // depth of this plugin in the dependency tree (root plugins will have depth = 1)
|
||||
dependencies: PluginName[];
|
||||
reverseDependencies: PluginName[];
|
||||
reportedStatus?: PluginStatus;
|
||||
|
@ -81,7 +79,8 @@ export class PluginsStatusService {
|
|||
constructor(deps: Deps, private readonly statusTimeoutMs: number = STATUS_TIMEOUT_MS) {
|
||||
this.pluginData = this.initPluginData(deps.pluginDependencies);
|
||||
this.rootPlugins = this.getRootPlugins();
|
||||
this.orderedPluginNames = this.getOrderedPluginNames();
|
||||
// plugin dependencies keys are already sorted
|
||||
this.orderedPluginNames = [...deps.pluginDependencies.keys()];
|
||||
|
||||
this.coreSubscription = deps.core$
|
||||
.pipe(
|
||||
|
@ -216,23 +215,20 @@ export class PluginsStatusService {
|
|||
private initPluginData(pluginDependencies: ReadonlyMap<PluginName, PluginName[]>): PluginData {
|
||||
const pluginData: PluginData = {};
|
||||
|
||||
if (pluginDependencies) {
|
||||
pluginDependencies.forEach((dependencies, name) => {
|
||||
pluginData[name] = {
|
||||
name,
|
||||
depth: 0,
|
||||
dependencies,
|
||||
reverseDependencies: [],
|
||||
derivedStatus: defaultStatus,
|
||||
};
|
||||
});
|
||||
pluginDependencies.forEach((dependencies, name) => {
|
||||
pluginData[name] = {
|
||||
name,
|
||||
dependencies,
|
||||
reverseDependencies: [],
|
||||
derivedStatus: defaultStatus,
|
||||
};
|
||||
});
|
||||
|
||||
pluginDependencies.forEach((dependencies, name) => {
|
||||
dependencies.forEach((dependency) => {
|
||||
pluginData[dependency].reverseDependencies.push(name);
|
||||
});
|
||||
pluginDependencies.forEach((dependencies, name) => {
|
||||
dependencies.forEach((dependency) => {
|
||||
pluginData[dependency].reverseDependencies.push(name);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
return pluginData;
|
||||
}
|
||||
|
@ -248,36 +244,6 @@ export class PluginsStatusService {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Obtain a list of plugins names, ordered by depth.
|
||||
* @see {calculateDepthRecursive}
|
||||
* @returns {PluginName[]} a list of plugins, ordered by depth + name
|
||||
*/
|
||||
private getOrderedPluginNames(): PluginName[] {
|
||||
this.rootPlugins.forEach((plugin) => {
|
||||
this.calculateDepthRecursive(plugin, 1);
|
||||
});
|
||||
|
||||
return sortBy(Object.values(this.pluginData), ['depth', 'name']).map(({ name }) => name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the depth of the given plugin, knowing that it's has at least the specified depth
|
||||
* The depth of a plugin is determined by how many levels of dependencies the plugin has above it.
|
||||
* We define root plugins as depth = 1, plugins that only depend on root plugins will have depth = 2
|
||||
* and so on so forth
|
||||
* @param {PluginName} plugin the name of the plugin whose depth must be calculated
|
||||
* @param {number} depth the minimum depth that we know for sure this plugin has
|
||||
*/
|
||||
private calculateDepthRecursive(plugin: PluginName, depth: number): void {
|
||||
const pluginData = this.pluginData[plugin];
|
||||
pluginData.depth = Math.max(pluginData.depth, depth);
|
||||
const newDepth = depth + 1;
|
||||
pluginData.reverseDependencies.forEach((revDep) =>
|
||||
this.calculateDepthRecursive(revDep, newDepth)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the root plugins statuses according to the current core services status
|
||||
*/
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue