start-up visualizer: report and visualize extensions

This commit is contained in:
Vladimir Krivosheev
2019-03-05 19:59:44 +01:00
parent 357a1315c5
commit b32d2a2749
14 changed files with 174 additions and 87 deletions
@@ -14,6 +14,21 @@
<style>
.activityChart {
width: 100%;
/*
our data has extraordinary high values (extremes) and it makes item chart not readable (extremes are visible and others column bars are too low),
as solution, amCharts supports breaks (https://www.amcharts.com/demos/column-chart-with-axis-break/),
but it contradicts to our goal - to show that these items are extremes,
so, as solution, we increase chart height to give more space to render bars.
It is ok, as now we use UI Library (ElementUI) and can use Tabs, Collapse and any other component to group charts.
Also, as we use Vue.js and Vue Router, it is one-line to provide dedicated view (/#/components and so on)
*/
height: 500px;
}
.timeLineChart {
width: 100%;
/* in any case time line chart will adjust height according to items */
height: 380px;
}
</style>
@@ -15,26 +15,33 @@ export abstract class BaseChartComponent<T extends ChartManager> extends Vue {
}
mounted() {
this.chartManager = this.createChartManager()
this.renderDataIfAvailable()
}
protected abstract createChartManager(): T
@Watch("measurementData")
// @ts-ignore
private measurementDataChanged(): void {
this.renderDataIfAvailable()
}
/** @final */
protected renderDataIfAvailable(): void {
const data = this.measurementData
if (data != null) {
const chartManager = this.chartManager
if (chartManager != null) {
chartManager.render(data)
}
if (data == null) {
// yes, do not re-render as empty - null value not expected to be set in valid cases
return
}
let chartManager = this.chartManager
if (chartManager == null) {
chartManager = this.createChartManager()
this.chartManager = chartManager
}
chartManager.render(data)
}
beforeDestroy() {
const chartManager = this.chartManager
if (chartManager != null) {
this.chartManager = null
chartManager.dispose()
}
}
}
@@ -5,6 +5,8 @@ import {DataManager} from "@/state/DataManager"
export interface ChartManager {
render(data: DataManager): void
dispose(): void
}
function configureCommonChartSettings(chart: am4charts.XYChart) {
@@ -15,7 +17,6 @@ function configureCommonChartSettings(chart: am4charts.XYChart) {
const cursor = new am4charts.XYCursor()
cursor.lineY.disabled = true
cursor.lineX.disabled = true
// todo y axis for ItemChart doesn't work as expected (not scaled according to current data) because of 2 series for axis (and so, no chart data is set)
cursor.behavior = "zoomXY"
chart.cursor = cursor
}
@@ -30,30 +31,10 @@ export abstract class XYChartManager implements ChartManager {
// this.addDisposeHandler(childHot)
}
/** @override */
dispose(): void {
this.chart.dispose()
}
// module.hot must be passed here explicitly, because module in this context related only to this module
// private addDisposeHandler(hot: __WebpackModuleApi.Hot | null | undefined) {
// if (hot == null) {
// return
// }
//
// hot.dispose(_data => {
// const chart = this.chart
// if (chart == null) {
// return
// }
//
// (this as any).chart = null
// chart.dispose()
// // const exportingMenu = chart.exporting.menu
// // if (exportingMenu != null) {
// // exportingMenu.dispose()
// // }
// })
// }
abstract render(data: DataManager): void
}
@@ -5,7 +5,7 @@
<script lang="ts">
import {Component, Prop, Watch} from "vue-property-decorator"
import {ComponentChartManager, ItemChartManager, ServiceChartManager, TopHitProviderChart} from "./ItemChartManager"
import {ComponentChartManager, ItemChartManager} from "./ItemChartManager"
import {ItemChartType} from "@/charts/ItemChartDescriptor"
import {BaseChartComponent} from "@/charts/BaseChartComponent"
@@ -34,10 +34,13 @@
return new ComponentChartManager(chartContainer)
}
else if (type === "services") {
return new ServiceChartManager(chartContainer)
return new ItemChartManager(chartContainer, ["appServices", "projectServices", "moduleServices"])
}
else if (type === "extensions") {
return new ItemChartManager(chartContainer, ["appExtensions", "projectExtensions", "moduleExtensions"])
}
else if (type === "topHitProviders") {
return new TopHitProviderChart(chartContainer)
return new ItemChartManager(chartContainer, ["appOptionsTopHitProviders", "projectOptionsTopHitProviders"])
}
else {
throw new Error(`Unknown chart type: ${type}`)
@@ -1,6 +1,6 @@
// Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file.
export type ItemChartType = "components" | "services" | "topHitProviders"
export type ItemChartType = "components" | "services" | "extensions" | "topHitProviders"
export interface ItemChartDescriptor {
readonly label: string
@@ -8,9 +8,10 @@ export interface ItemChartDescriptor {
}
// not as part of ItemChartManager.ts to reduce scope of changes on change
// (makes sure that hot reloading to not reload all modules where `chartDescriptors` is used - especially `router`)
// (make sure that hot reloading will not reload all modules where `chartDescriptors` is used - especially `router`)
export const chartDescriptors: Array<ItemChartDescriptor> = [
{label: "Components", name: "components"},
{label: "Services", name: "services"},
{label: "Extensions", name: "extensions"},
{label: "Options Top Hit Providers", name: "topHitProviders"},
]
@@ -5,13 +5,18 @@ import {XYChartManager} from "@/charts/ChartManager"
import {DataManager} from "@/state/DataManager"
import {Item} from "@/state/data"
export type ComponentProviderSourceNames = "appComponents" | "projectComponents"
export type ServiceProviderSourceNames = "appServices" | "projectServices"
export type ComponentProviderSourceNames = "appComponents" | "projectComponents" | "moduleComponents"
export type ServiceProviderSourceNames = "appServices" | "projectServices" | "moduleServices"
export type ExtensionProviderSourceNames = "appExtensions" | "projectExtensions" | "moduleExtensions"
export type TopHitProviderSourceNames = "appOptionsTopHitProviders" | "projectOptionsTopHitProviders"
export abstract class ItemChartManager extends XYChartManager {
export class ItemChartManager extends XYChartManager {
// isUseYForName - if true, names are more readable, but not possible to see all components because layout from top to bottom (so, opposite from left to right some data can be out of current screen)
protected constructor(container: HTMLElement, private readonly sourceNames: Array<ComponentProviderSourceNames> | Array<TopHitProviderSourceNames> | Array<ServiceProviderSourceNames>) {
constructor(container: HTMLElement,
private readonly sourceNames: Array<ComponentProviderSourceNames>
| Array<TopHitProviderSourceNames>
| Array<ServiceProviderSourceNames>
| Array<ExtensionProviderSourceNames>) {
super(container, module.hot)
this.configureNameAxis()
@@ -50,14 +55,21 @@ export abstract class ItemChartManager extends XYChartManager {
private configureDurationAxis(): void {
const durationAxis = this.chart.yAxes.push(new am4charts.DurationAxis())
durationAxis.title.text = "Duration"
durationAxis.baseUnit = "millisecond"
// base unit the values are in (https://www.amcharts.com/docs/v4/reference/durationformatter/)
durationAxis.durationFormatter.baseUnit = "millisecond"
durationAxis.durationFormatter.durationFormat = "S"
// do not set strictMinMax because otherwise zooming will not work
// (axis will not reflect currently selected items, but always the whole set and so, will be not convenient to see items with small values)
// durationAxis.strictMinMax = true
// cursor tooltip is distracting
durationAxis.cursorTooltipEnabled = false
// https://www.amcharts.com/docs/v4/concepts/axes/positioning-axis-elements/#Setting_the_density_of_the_the_grid_labels
// default value 40 makes distribution not good
// but for other charts default value suits better...
// decided to not do anything - if you don't like that extremes makes chart not readable - fix these extremes or select desired area (cursor is supported)
}
protected configureSeries(): void {
@@ -69,7 +81,7 @@ export abstract class ItemChartManager extends XYChartManager {
}
// https://www.amcharts.com/docs/v4/concepts/series/#Note_about_Series_data_and_Category_axis
render(data: DataManager) {
render(data: DataManager): void {
const concatenatedData: Array<ClassItem> = []
let colorIndex = 0
const legendData: Array<LegendItem> = []
@@ -83,17 +95,21 @@ export abstract class ItemChartManager extends XYChartManager {
stroke: color,
}
// generate color before - even if no data for this type of items, still color should be the same regardless of current data set
// so, if currently no data for project, but there is data for modules, color for modules should use index 3 and not 2
const items = data.data[sourceName]
if (items == null || items.length === 0) {
continue
}
const legendItem: LegendItem = {
name: (sourceName.startsWith("app") ? "Application" : "Project") + "-level",
name: this.sourceNameToLegendName(sourceName, items.length),
fill: color,
sourceName,
}
legendData.push(legendItem)
applicableSources.add(sourceName)
// generate color before - even if no data for this type of items, still color should be the same regardless of current data set
// so, if currently no data for project, but there is data for modules, color for modules should use index 3 and not 2
const items = data.data[sourceName] || []
for (const item of items) {
concatenatedData.push({
...item,
@@ -123,7 +139,21 @@ export abstract class ItemChartManager extends XYChartManager {
this.chart.data = concatenatedData
}
protected computeRangeMarkers(data: DataManager, items: Array<ClassItem>) {
private sourceNameToLegendName(sourceName: string, itemCount: number): string {
let prefix
if (sourceName.startsWith("app")) {
prefix = "Application"
}
else if (sourceName.startsWith("project")) {
prefix = "Project"
}
else if (sourceName.startsWith("module")) {
prefix = "Module"
}
return `${prefix}-level (${itemCount})`
}
protected computeRangeMarkers(data: DataManager, items: Array<ClassItem>): void {
const nameAxis = this.nameAxis
nameAxis.axisRanges.clear()
for (const guideLineDescriptor of data.computeGuides(items)) {
@@ -131,7 +161,7 @@ export abstract class ItemChartManager extends XYChartManager {
}
}
private createRangeMarker(axis: am4charts.CategoryAxis, item: ClassItem, label: string) {
private createRangeMarker(axis: am4charts.CategoryAxis, item: ClassItem, label: string): void {
const range = axis.axisRanges.create()
range.category = item.shortName
range.label.inside = true
@@ -160,7 +190,7 @@ function getShortName(item: Item): string {
export class ComponentChartManager extends ItemChartManager {
constructor(container: HTMLElement) {
super(container, ["appComponents", "projectComponents"])
super(container, ["appComponents", "projectComponents", "moduleComponents"])
}
// doesn't make sense for components - cannot be outside of ready, and app initialized is clear
@@ -169,18 +199,6 @@ export class ComponentChartManager extends ItemChartManager {
}
}
export class ServiceChartManager extends ItemChartManager {
constructor(container: HTMLElement) {
super(container, ["appServices", "projectServices"])
}
}
export class TopHitProviderChart extends ItemChartManager {
constructor(container: HTMLElement) {
super(container, ["appOptionsTopHitProviders", "projectOptionsTopHitProviders"])
}
}
interface LegendItem {
readonly name: string
readonly sourceName: string
@@ -16,9 +16,15 @@ export interface InputData {
appComponents?: Array<Item>
projectComponents?: Array<Item>
moduleComponents?: Array<Item>
appServices?: Array<Item>
projectServices?: Array<Item>
moduleServices?: Array<Item>
appExtensions?: Array<Item>
projectExtensions?: Array<Item>
moduleExtensions?: Array<Item>
preloadActivities?: Array<Item>
appOptionsTopHitProviders?: Array<Item>
@@ -1,6 +1,6 @@
<!-- Copyright 2000-2019 JetBrains s.r.o. Use of this source code is governed by the Apache 2.0 license that can be found in the LICENSE file. -->
<template>
<div class="activityChart" ref="chartContainer"></div>
<div class="timeLineChart" ref="chartContainer"></div>
</template>
<script lang="ts">
@@ -44,6 +44,8 @@ import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import static com.intellij.util.pico.DefaultPicoContainer.getMeasureTokenLevel;
public abstract class ComponentManagerImpl extends UserDataHolderBase implements ComponentManagerEx, Disposable {
private static final Logger LOG = Logger.getInstance("#com.intellij.components.ComponentManager");
@@ -481,9 +483,7 @@ public abstract class ComponentManagerImpl extends UserDataHolderBase implements
return instance;
}
// if it will be module component, then get rid of such component instead of measurement
boolean appComponent = ComponentManagerImpl.this instanceof Application;
StartUpMeasurer.MeasureToken measureToken = StartUpMeasurer.start(appComponent ? Activities.APP_COMPONENT : Activities.PROJECT_COMPONENT);
StartUpMeasurer.MeasureToken measureToken = StartUpMeasurer.start(Activities.COMPONENT, getMeasureTokenLevel(picoContainer));
instance = super.getComponentInstance(picoContainer);
if (myInitializing) {
@@ -10,6 +10,8 @@ import com.intellij.openapi.util.EmptyRunnable;
import com.intellij.openapi.util.Pair;
import com.intellij.util.ArrayFactory;
import com.intellij.util.ArrayUtil;
import com.intellij.util.StartUpMeasurer;
import com.intellij.util.StartUpMeasurer.Activities;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.OpenTHashSet;
import org.jdom.Element;
@@ -25,6 +27,8 @@ import java.util.function.BiPredicate;
import java.util.function.Predicate;
import java.util.stream.Stream;
import static com.intellij.util.pico.DefaultPicoContainer.getMeasureTokenLevel;
/**
* @author AKireyev
*/
@@ -272,6 +276,8 @@ public abstract class ExtensionPointImpl<T> implements ExtensionPoint<T> {
}
assertNotReadOnlyMode();
StartUpMeasurer.MeasureToken measureToken = StartUpMeasurer.start(Activities.EXTENSION, getMeasureTokenLevel(myPicoContainer));
int totalSize = myAdapters.size();
Class<T> extensionClass = getExtensionClass();
T[] result = ArrayUtil.newArray(extensionClass, totalSize);
@@ -281,7 +287,6 @@ public abstract class ExtensionPointImpl<T> implements ExtensionPoint<T> {
// check before to avoid any "restore" work if already cancelled
CHECK_CANCELED.run();
processingAdaptersNow = true;
try {
List<ExtensionComponentAdapter> adapters = myAdapters;
@@ -332,6 +337,9 @@ public abstract class ExtensionPointImpl<T> implements ExtensionPoint<T> {
if (extensionIndex != result.length) {
result = Arrays.copyOf(result, extensionIndex);
}
// don't count ProcessCanceledException as valid action to measure (later special category can be introduced if needed)
measureToken.endWithThreshold(extensionClass);
return result;
}
finally {
@@ -3,6 +3,7 @@ package com.intellij.util.pico;
import com.intellij.util.ReflectionUtil;
import com.intellij.util.SmartList;
import com.intellij.util.StartUpMeasurer;
import com.intellij.util.containers.ContainerUtil;
import com.intellij.util.containers.FList;
import gnu.trove.THashSet;
@@ -403,4 +404,18 @@ public class DefaultPicoContainer implements MutablePicoContainer {
public String toString() {
return "DefaultPicoContainer" + (getParent() == null ? " (root)" : " (parent="+getParent()+")");
}
@NotNull
public static StartUpMeasurer.Level getMeasureTokenLevel(@NotNull PicoContainer picoContainer) {
PicoContainer parent = picoContainer.getParent();
if (parent == null) {
return StartUpMeasurer.Level.APPLICATION;
}
else if (parent.getParent() == null) {
return StartUpMeasurer.Level.PROJECT;
}
else {
return StartUpMeasurer.Level.MODULE;
}
}
}
@@ -86,12 +86,16 @@ class StartUpPerformanceReporter : StartupActivity, DumbAware {
private fun logStats(end: Long, activationNumber: Int) {
val items = mutableListOf<Item>()
val activities = THashMap<String, MutableList<Item>>()
StartUpMeasurer.processAndClear(Consumer { item ->
if (item.name.first() == '_') {
activities.getOrPut(item.name) { mutableListOf() }.add(item)
fun addActivity(name: String) {
activities.getOrPut(name) { mutableListOf() }.add(item)
}
else {
items.add(item)
when {
item.name.first() == '_' -> addActivity(item.name)
item.level != null -> addActivity("${item.level!!.jsonFieldNamePrefix}${item.name.capitalize()}")
else -> items.add(item)
}
})
@@ -161,9 +165,10 @@ class StartUpPerformanceReporter : StartupActivity, DumbAware {
}
private fun activityNameToJsonFieldName(name: String): String {
val firstIndex = if (name.startsWith('_')) 1 else 0
return when {
name.last() == 'y' -> name.substring(1, name.length - 1) + "ies"
else -> name.substring(1) + 's'
name.last() == 'y' -> name.substring(firstIndex, name.length - 1) + "ies"
else -> name.substring(firstIndex) + 's'
}
}
@@ -41,6 +41,8 @@ import java.util.List;
import java.util.function.BiPredicate;
import java.util.function.Consumer;
import static com.intellij.util.pico.DefaultPicoContainer.getMeasureTokenLevel;
public final class ServiceManagerImpl implements Disposable {
private static final Logger LOG = Logger.getInstance(ServiceManagerImpl.class);
@@ -231,9 +233,7 @@ public final class ServiceManagerImpl implements Disposable {
@NotNull
private Object createAndInitialize(@NotNull PicoContainer container) {
// if it will be module service, then get rid of such component instead of measurement
boolean appComponent = myComponentManager instanceof Application;
StartUpMeasurer.MeasureToken measureToken = StartUpMeasurer.start(appComponent ? Activities.APP_SERVICE : Activities.PROJECT_SERVICE);
StartUpMeasurer.MeasureToken measureToken = StartUpMeasurer.start(Activities.SERVICE, getMeasureTokenLevel(container));
Object instance = getDelegate().getComponentInstance(container);
if (instance instanceof Disposable) {
Disposer.register(myComponentManager, (Disposable)instance);
@@ -42,13 +42,27 @@ public final class StartUpMeasurer {
public static final String LOAD_MODULES = "module loading";
}
// ExtensionAreas not available for ExtensionPointImpl
public enum Level {
APPLICATION("app"), PROJECT("project"), MODULE("module");
private final String jsonFieldNamePrefix;
Level(@NotNull String jsonFieldNamePrefix) {
this.jsonFieldNamePrefix = jsonFieldNamePrefix;
}
@NotNull
public String getJsonFieldNamePrefix() {
return jsonFieldNamePrefix;
}
}
// non-sequential and repeated items
public static final class Activities {
public static final String APP_COMPONENT = "_appComponent";
public static final String PROJECT_COMPONENT = "_projectComponent";
public static final String APP_SERVICE = "_appService";
public static final String PROJECT_SERVICE = "_projectService";
public static final String COMPONENT = "component";
public static final String SERVICE = "service";
public static final String EXTENSION = "extension";
public static final String PRELOAD_ACTIVITY = "_preloadActivity";
@@ -66,12 +80,17 @@ public final class StartUpMeasurer {
@NotNull
public static MeasureToken start(@NotNull String name, @Nullable String description) {
return new Item(name, description);
return new Item(name, description, null);
}
@NotNull
public static MeasureToken start(@NotNull String name) {
return new Item(name, null);
return new Item(name, null, null);
}
@NotNull
public static MeasureToken start(@NotNull String name, @NotNull Level level) {
return new Item(name, null, level);
}
public static void processAndClear(@NotNull Consumer<Item> consumer) {
@@ -95,15 +114,19 @@ public final class StartUpMeasurer {
// null doesn't mean root - not obligated to set parent, only as hint
private final Item parent;
private Item(@Nullable String name, @Nullable String description) {
this(name, description, System.nanoTime(), null);
@Nullable
private final Level myLevel;
private Item(@Nullable String name, @Nullable String description, @Nullable Level level) {
this(name, description, System.nanoTime(), null, level);
}
private Item(@Nullable String name, @Nullable String description, long start, @Nullable Item parent) {
private Item(@Nullable String name, @Nullable String description, long start, @Nullable Item parent, @Nullable Level level) {
this.name = name;
this.description = StringUtil.nullize(description);
this.start = start;
this.parent = parent;
myLevel = level;
}
@Nullable
@@ -111,12 +134,17 @@ public final class StartUpMeasurer {
return parent;
}
@Nullable
public Level getLevel() {
return myLevel;
}
// and how do we can sort correctly, when parent item equals to child (start and end) and also there is another child with start equals to end?
// so, parent added to API but as it was not enough, decided to measure time in nanoseconds instead of ms to mitigate such situations
@Override
@NotNull
public Item startChild(@NotNull String name) {
return new Item(name, null, System.nanoTime(), this);
return new Item(name, null, System.nanoTime(), this, null);
}
@NotNull