start-up visualizer: rewrite in vuejs and ElementUI

Reason — no easy way to implement tabs (required component) in Vanilla JS. It is possible but time consuming and reinventing the wheel. It tool only 20 minutes to implement tab js, but what about style? What about routing (ability to navigate and persist selected tab via URL query). So, it is time to move to some framework and do not resist.

1. https://github.com/JetBrains/ring-ui is not chosen because React is required.
2. Vue.js is used instead of React because two years ago I selected Vue.js and now main goal was to implement new functionality but not spend time choosing between frameworks again.
3. Not confident that ring-ui quality is better than ElementUI - amount of time to get good looking UI without touching any line of CSS (main problem here not CSS itself, but design — designer skills).
4. Vuetify is not used because MD and Google. Feeling is not great.
5. iView looks like ElementUI and two years ago I selected iView, but now ElementUI has more stars (35,427 vs 19,984), doesn't have silly critical blocking issue in TS support (time.ts 'VNode' is declared but its value is never read.)

Main reason to not choose ElementUI was fact that ElementUI is not so contrast and readable. But now... it looks decent, especially on Retina screen. Also, for this app such a little bit gray text is good because doesn't distract from charts.

(no doubt - it is easy to change font color, but point here is that UI framework should be good looking without any tweaks, otherwise it is strong indicator that other aspects will require tuning also)
This commit is contained in:
Vladimir Krivosheev
2019-03-03 08:41:03 +01:00
parent b4269a7f38
commit 26d3d3ee39
30 changed files with 2848 additions and 2700 deletions
@@ -1,16 +0,0 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Start-up Timeline</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="stylesheet" type="text/css" href="main.css">
</head>
<body>
<div id="visualization" class="activityChart"></div>
<script src="src/dev.ts"></script>
</body>
</html>
@@ -1,55 +0,0 @@
<!doctype html>
<!--suppress HtmlFormInputWithoutLabel -->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Start-up Timeline</title>
<link rel="icon" href="data:;base64,iVBORw0KGgo=">
<link rel="stylesheet" type="text/css" href="main.css">
<!-- for security reasons official source of scripts is not used - versioned URL must be used -->
<!-- jsdelivr cannot handle amcharts package (>50MB), so, use https://www.srihash.org/ to compute integrity -->
<script src="https://cdn.jsdelivr.net/gh/amcharts/amcharts4@4.1.13/dist/script/core.js" integrity="sha384-0HDDTNDAhQn9j4s7gmgU6BqbINySsv1pNylznb13zwJkJrRse54yhyWVKmAbMhl0" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/amcharts/amcharts4@4.1.13/dist/script/charts.js" integrity="sha384-bfPyQpCTkSYRPZUdnLxTkTvPAya+trlCw3p88R3V0FYrp6gETXtkydsXEK3aGIJk" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/amcharts/amcharts4@4.1.13/dist/script/themes/animated.js" integrity="sha384-2WR4/PAikFSwy8DTue1g8Cb6Bht0j1T3VoB1WthzSBLIf98n5BHhNHtOmfcPsqvk" crossorigin="anonymous"></script>
</head>
<body>
<div class="container">
<!--suppress CheckTagEmptyBody -->
<textarea id="ijInput" placeholder="Enter the IntelliJ Platform start-up timeline..." autocomplete="off"
cols="64" rows="4"></textarea>
<div>
<button id="grabButton">Get from running instance</button>
<input id="ijPort" type="number" min="1024" max="65535"/>
<div style="padding-top: 0.5em">
<button id="grabDevButton" title="Get from running instance on port 63343">Get from running dev instance</button>
</div>
</div>
</div>
<p><small>For timeline no threshold, for other charts threshold is 10ms. End time equals to last dumb-aware project start-up activity (so, preload activities may be out of end time).</small></p>
<h2>Timeline</h2>
<div id="visualization"></div>
<h2>Components</h2>
<div id="componentChart" class="activityChart"></div>
<h2>Services</h2>
<div id="serviceChart" class="activityChart"></div>
<h2>Options Top Hit Providers</h2>
<div id="optionsTopHitProviderChart" class="activityChart"></div>
<br/>
<hr>
<ul>
<li><small>app initialized: end of phase <code>app initialized callback</code>.</small></li>
<li><small>project initialized: end of phase <code>module loading</code>.</small></li>
</ul>
<br/>
</body>
</html>
@@ -1,18 +0,0 @@
/* 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. */
body, html {
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
}
.activityChart {
width: 100%;
height: 380px;
}
/* both bootstrap and bulma uglify look (large paddings, customized components and so on) */
/* https://blog.theodo.fr/2018/03/stop-using-bootstrap-layout-thanks-to-css-grid/ */
.container {
/* https://stackoverflow.com/a/47171358 */
display: inline-grid;
grid-template-columns: auto auto;
grid-gap: 1em;
}
@@ -1,36 +1,36 @@
{
"name": "start-up-visualizer",
"version": "0.1.0",
"private": true,
"scripts": {
"serve": "vue-cli-service serve",
"build": "vue-cli-service build"
},
"dependencies": {
"@amcharts/amcharts4": "4.1.13"
"@amcharts/amcharts4": "4.1.14",
"element-ui": "^2.6.0",
"vue": "^2.6.7",
"vue-class-component": "^7.0.1",
"vue-property-decorator": "^7.3.0",
"vue-router": "^3.0.2",
"vuex": "^3.1.0",
"vuex-persist": "^2.0.0"
},
"devDependencies": {
"@types/jest": "^24.0.9",
"@types/webpack-env": "^1.13.7",
"clean-webpack-plugin": "^1.0.1",
"copy-webpack-plugin": "^5.0.0",
"fork-ts-checker-notifier-webpack-plugin": "^1.0.0-alpha.1",
"fork-ts-checker-webpack-plugin": "^1.0.0-alpha.8",
"html-webpack-plugin": "^3.2.0",
"jest": "^24.1.0",
"ts-jest": "^24.0.0",
"ts-loader": "^5.3.3",
"@vue/cli-plugin-typescript": "^3.4.1",
"@vue/cli-service": "^3.4.1",
"typescript": "^3.3.3333",
"webpack": "^4.29.5",
"webpack-cli": "^3.2.3",
"webpack-dev-server": "^3.2.1",
"webpack-subresource-integrity": "^1.3.2"
"vue-template-compiler": "^2.6.7",
"vuex-module-decorators": "^0.9.8"
},
"scripts": {
"//": "chrome://flags/#allow-insecure-localhost to tell Chrome to ignore cert warnings on localhost",
"dist": "webpack --mode=production",
"dist-ij": "webpack --mode=production --optimize-minimize=false",
"dev": "webpack-dev-server",
"dev-one": "webpack-dev-server",
"test": "jest"
"postcss": {
"plugins": {
"autoprefixer": {}
}
},
"jest": {
"preset": "ts-jest",
"testEnvironment": "node"
}
"browserslist": [
"> 1%",
"last 2 versions",
"not ie <= 8"
]
}
@@ -0,0 +1,24 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width,initial-scale=1.0">
<title>Start-up Visualizer</title>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.6.0/lib/theme-chalk/reset.css" integrity="sha256-Rxj/ya8a4hewBMy8DOozoDvUFDw3F63poVzER+ru0OQ=" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/element-ui@2.6.0/lib/theme-chalk/index.css" integrity="sha256-6N9DJqVdUNwu+mIUDrJacQmdCGg2Xs4WABEM4Pyn/lM=" crossorigin="anonymous">
</head>
<body>
<noscript>
<strong>We're sorry but start-up-visualizer doesn't work properly without JavaScript enabled. Please enable it to continue.</strong>
</noscript>
<div id="app"></div>
<!--<script src="https://cdn.jsdelivr.net/npm/element-ui@2.6.0/lib/index.js" integrity="sha256-MTGzwqBB+bPE/tjByLlC1h3mfIPqUdkQRhwcgvMTQrk=" crossorigin="anonymous"></script>-->
<script src="https://cdn.jsdelivr.net/gh/amcharts/amcharts4@4.1.14/dist/script/core.js" integrity="sha384-9tCvqYGsm6wSpJWzZ/1WcRlJm4gIWUyA+BdEmmoTs415DeDu74Nl0B0zUaT59Dpw" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/amcharts/amcharts4@4.1.14/dist/script/charts.js" integrity="sha384-6ll1B4OF1zbH9+LafaZ+RzGFLpStsDrspDpreEqCfW7EOXN/qRjebvCnp/K0VP/N" crossorigin="anonymous"></script>
<script src="https://cdn.jsdelivr.net/gh/amcharts/amcharts4@4.1.14/dist/script/themes/animated.js" integrity="sha384-2WR4/PAikFSwy8DTue1g8Cb6Bht0j1T3VoB1WthzSBLIf98n5BHhNHtOmfcPsqvk" crossorigin="anonymous"></script>
<!-- built files will be auto injected -->
</body>
</html>
@@ -0,0 +1,19 @@
<!-- 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>
<router-view/>
</template>
<script lang="ts">
import {Component, Vue} from "vue-property-decorator"
import Main from "@/views/Main.vue"
@Component({components: {Main}})
export default class App extends Vue {
}
</script>
<style>
.activityChart {
width: 100%;
height: 380px;
}
</style>
@@ -0,0 +1,13 @@
<!-- 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>
<ItemChart type="components"/>
</template>
<script lang="ts">
import {Component, Vue} from "vue-property-decorator"
import ItemChart from "./ItemChart.vue"
@Component({components: {ItemChart}})
export default class ComponentChart extends Vue {
}
</script>
@@ -0,0 +1,51 @@
<!-- 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>
</template>
<script lang="ts">
import {Component, Prop, Vue} from "vue-property-decorator"
import {ComponentChartManager, ItemChartType, ServiceChartManager, TopHitProviderChart} from "./ItemChartManager"
import {DataManager, DataModule, UPDATE_DATE_MUTATION_NAME} from "@/state"
import {getModule} from "vuex-module-decorators"
import {ChartManager} from "@/core"
@Component
export default class ItemChart extends Vue {
@Prop(String)
type!: ItemChartType
private readonly dataModule = getModule(DataModule, this.$store)
mounted() {
const chartManager = this.createChartManager()
this.$store.subscribe(mutation => {
if (mutation.type === UPDATE_DATE_MUTATION_NAME) {
chartManager.render(new DataManager(this.dataModule))
}
})
const data = this.dataModule.data
if (data != null) {
chartManager.render(new DataManager(this.dataModule))
}
}
private createChartManager(): ChartManager {
const chartContainer = this.$refs.chartContainer as HTMLElement
const type = this.type
if (type === "components" || type == null) {
return new ComponentChartManager(chartContainer)
}
else if (type === "services") {
return new ServiceChartManager(chartContainer)
}
else if (type === "topHitProviders") {
return new TopHitProviderChart(chartContainer)
}
else {
throw new Error(`Unknown chart type: ${type}`)
}
}
}
</script>
@@ -1,13 +1,16 @@
// 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.
import * as am4charts from "@amcharts/amcharts4/charts"
import {XYChartManager} from "./core"
import * as am4core from "@amcharts/amcharts4/core"
import {DataManager, Item} from "./data"
import {DataManager} from "@/state"
import {XYChartManager} from "@/core"
import {Item} from "@/data"
export type ComponentProviderSourceNames = "appComponents" | "projectComponents"
export type ServiceProviderSourceNames = "appServices" | "projectServices"
export type TopHitProviderSourceNames = "appOptionsTopHitProviders" | "projectOptionsTopHitProviders"
export type ItemChartType = "components" | "services" | "topHitProviders"
export abstract 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>) {
@@ -159,8 +162,9 @@ export abstract class ItemChartManager extends XYChartManager {
range.label.adapter.add("dy", (_y, _target) => {
return -this.chart.yAxes.getIndex(0)!!.pixelHeight
})
range.label.adapter.add("x", (_x, _target) => {
return range.point.x
range.label.adapter.add("x", (x, _target) => {
const rangePoint = range.point
return rangePoint == null ? x : rangePoint.x
})
}
@@ -0,0 +1,13 @@
<!-- 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>
<ItemChart type="services"/>
</template>
<script lang="ts">
import {Component, Vue} from "vue-property-decorator"
import ItemChart from "./ItemChart.vue"
@Component({components: {ItemChart}})
export default class ServiceChart extends Vue {
}
</script>
@@ -1,9 +1,10 @@
// 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.
import {XYChartManager} from "./core"
import {XYChartManager} from "@/core"
import * as am4charts from "@amcharts/amcharts4/charts"
import * as am4core from "@amcharts/amcharts4/core"
import {computeLevels, disableGridButKeepBorderLines, TimeLineItem} from "./timeLineChartHelper"
import {DataManager, InputData} from "./data"
import {InputData} from "@/data"
import {DataManager} from "@/state"
export class TimelineChartManager extends XYChartManager {
private maxRowIndex = 0
@@ -115,13 +116,11 @@ export class TimelineChartManager extends XYChartManager {
this.maxRowIndex = rowIndex
}
const result: any = {
transformedItems[i] = {
...item,
rowIndex: rowIndex++,
color: colorSet.getIndex(item.colorIndex),
}
transformedItems[i] = result
}
transformedItems.sort((a, b) => a.rowIndex - b.rowIndex)
@@ -0,0 +1,13 @@
<!-- 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>
<ItemChart type="topHitProviders"/>
</template>
<script lang="ts">
import {Component, Vue} from "vue-property-decorator"
import ItemChart from "./ItemChart.vue"
@Component({components: {ItemChart}})
export default class TopHitOptionPreloaderChart extends Vue {
}
</script>
@@ -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.
import * as am4charts from "@amcharts/amcharts4/charts"
import {Item} from "./data"
import {Item} from "@/data"
export interface TimeLineItem extends Item {
level: number
@@ -1,7 +1,7 @@
// 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.
import * as am4charts from "@amcharts/amcharts4/charts"
import * as am4core from "@amcharts/amcharts4/core"
import {DataManager} from "./data"
import {DataManager} from "@/state"
export interface ChartManager {
render(data: DataManager): void
@@ -23,92 +23,84 @@ function configureCommonChartSettings(chart: am4charts.XYChart) {
export abstract class XYChartManager implements ChartManager {
protected readonly chart: am4charts.XYChart
protected constructor(container: HTMLElement, childHot: __WebpackModuleApi.Hot | null | undefined) {
protected constructor(container: HTMLElement, _childHot: __WebpackModuleApi.Hot | null | undefined) {
this.chart = am4core.create(container, am4charts.XYChart)
configureCommonChartSettings(this.chart)
this.addDisposeHandler(childHot)
// this.addDisposeHandler(childHot)
if (module != null && module.hot != null) {
this.addDisposeHandler(module.hot)
let devState: DevState | null = null
const handler = () => {
const axis = this.chart.xAxes.getIndex(0)!!
if (devState == null) {
devState = {
start: axis.start,
end: axis.end,
}
}
else {
devState.start = axis.start
devState.end = axis.end
}
sessionStorage.setItem("devState", JSON.stringify(devState))
}
setTimeout(() => {
// noinspection SpellCheckingInspection
this.chart.xAxes.getIndex(0)!!.events.on("startchanged", handler)
// noinspection SpellCheckingInspection
this.chart.xAxes.getIndex(0)!!.events.on("endchanged", handler)
}, 1000)
// module.hot.dispose(() => {
// if (devState == null) {
// sessionStorage.removeItem("devState")
// }
// else {
// sessionStorage.setItem("devState", JSON.stringify(devState))
// }
// })
const devStateRaw = sessionStorage.getItem("devState")
if (devStateRaw != null) {
this.chart.events.on("ready", () => {
const devState = JSON.parse(devStateRaw)
const axis = this.chart.xAxes.getIndex(0)!!
axis.start = devState.start
axis.end = devState.end
})
}
}
// if (module != null && module.hot != null) {
// this.addDisposeHandler(module.hot)
//
// let devState: DevState | null = null
// const handler = () => {
// const axis = this.chart.xAxes.getIndex(0)!!
// if (devState == null) {
// devState = {
// start: axis.start,
// end: axis.end,
// }
// }
// else {
// devState.start = axis.start
// devState.end = axis.end
// }
//
// sessionStorage.setItem("devState", JSON.stringify(devState))
// }
// setTimeout(() => {
// // noinspection SpellCheckingInspection
// this.chart.xAxes.getIndex(0)!!.events.on("startchanged", handler)
// // noinspection SpellCheckingInspection
// this.chart.xAxes.getIndex(0)!!.events.on("endchanged", handler)
// }, 1000)
//
// // module.hot.dispose(() => {
// // if (devState == null) {
// // sessionStorage.removeItem("devState")
// // }
// // else {
// // sessionStorage.setItem("devState", JSON.stringify(devState))
// // }
// // })
//
// const devStateRaw = sessionStorage.getItem("devState")
// if (devStateRaw != null) {
// this.chart.events.on("ready", () => {
// const devState = JSON.parse(devStateRaw)
// const axis = this.chart.xAxes.getIndex(0)!!
// axis.start = devState.start
// axis.end = devState.end
// })
// }
// }
}
// 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()
// }
})
}
// 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
}
interface DevState {
start: number
end: number
}
export function getInputElement(id: string): HTMLInputElement {
return document.getElementById(id) as HTMLInputElement
}
export function getButtonElement(id: string): HTMLButtonElement {
return document.getElementById(id) as HTMLButtonElement
}
// interface DevState {
// start: number
// end: number
// }
@@ -27,84 +27,3 @@ export interface InputData {
totalDurationComputed: number
totalDurationActual: number
}
const markerNames = ["app initialized callback", "module loading"]
const markerNameToRangeTitle = new Map<string, string>([["app initialized callback", "app initialized"], ["module loading", "project initialized"]])
interface GuideLineDescriptor {
readonly item: Item
readonly label: string
}
export class DataManager {
readonly markerItems: Array<Item | null>
constructor(readonly data: InputData) {
const markerItems = new Array(markerNames.length)
// bloody JS array is sparse and setting length doesn't pre-fill array
markerItems.fill(null)
itemLoop: for (const item of data.items) {
for (let i = 0; i < markerNames.length; i++) {
if (markerItems[i] == null && item.name === markerNames[i]) {
markerItems[i] = item
// stop if all items are found
if (markerItems.every(it => it != null)) {
break itemLoop
}
}
}
}
for (let i = 0; i < markerNames.length; i++) {
if (markerItems[i] == null) {
console.error(`Cannot find item for phase "${markerNames[i]}"`)
}
}
this.markerItems = markerItems
}
computeGuides(items: Array<Item>): Array<GuideLineDescriptor> {
const rangeItems: Array<Item | null> = new Array(markerNames.length)
rangeItems.fill(null)
let outOfReady: Item | null = null
for (const item of items) {
for (let i = 0; i < this.markerItems.length; i++) {
if (rangeItems[i] == null) {
const markerItem = this.markerItems[i]
if (markerItem != null && item.start >= markerItem.end) {
rangeItems[i] = item
}
}
}
if (outOfReady == null && item.start >= this.data.totalDurationActual) {
outOfReady = item
}
if (outOfReady != null && rangeItems.every(it => it != null)) {
break
}
}
const result: Array<GuideLineDescriptor> = []
for (let i = 0; i < markerNames.length; i++) {
const rangeItem = rangeItems[i]
if (rangeItem != null) {
let rangeLabel = markerNames[i]
const customLabel = markerNameToRangeTitle.get(rangeLabel)
if (customLabel != null) {
rangeLabel = customLabel
}
result.push({item: rangeItem, label: rangeLabel})
}
}
if (outOfReady != null) {
result.push({item: outOfReady, label: "ready"})
}
return result
}
}
@@ -1,598 +0,0 @@
// 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.
import {TimelineChartManager} from "./TimeLineChartManager"
import {ComponentChartManager, TopHitProviderChart} from "./ItemChartManager"
import {DataManager, InputData} from "./data"
const data: InputData = {
"version": "1",
"items": [
{
"name": "app initialization preparation",
"duration": 2773,
"start": 1,
"end": 2774
},
{
"name": "app initialization",
"duration": 295,
"start": 2774,
"end": 3069
},
{
"name": "plugins initialization",
"description": "plugin count: 189",
"duration": 327,
"start": 3073,
"end": 3400
},
{
"name": "plugin descriptors loading",
"duration": 241,
"start": 3074,
"end": 3315
},
{
"name": "app components initialization",
"description": "component count: 101",
"duration": 3120,
"start": 3405,
"end": 6525
},
{
"name": "app components registration",
"duration": 381,
"start": 3405,
"end": 3786
},
{
"name": "app components registered callback",
"duration": 111,
"start": 3786,
"end": 3897
},
{
"name": "app components creation",
"duration": 2627,
"start": 3897,
"end": 6525
},
{
"name": "app initialized callback",
"duration": 229,
"start": 6525,
"end": 6754
},
{
"name": "project components initialization",
"description": "component count: 210",
"duration": 2026,
"start": 7547,
"end": 9574
},
{
"name": "project components registration",
"duration": 776,
"start": 7547,
"end": 8324
},
{
"name": "project components creation",
"duration": 1250,
"start": 8324,
"end": 9574
},
{
"name": "module loading",
"description": "module count: 1",
"duration": 143,
"start": 9575,
"end": 9718
},
{
"name": "project pre-startup",
"duration": 26,
"start": 11507,
"end": 11533
},
{
"name": "project startup",
"duration": 0,
"start": 11533,
"end": 11533
},
{
"name": "default project components initialization",
"description": "component count: 24",
"duration": 11,
"start": 11718,
"end": 11730
},
{
"name": "default project components registration",
"duration": 0,
"start": 11718,
"end": 11718
},
{
"name": "default project components creation",
"duration": 11,
"start": 11718,
"end": 11730
},
{
"name": "unknown",
"duration": 1475,
"start": 11730,
"end": 13205
}
],
"appComponents": [
{
"name": "com.intellij.openapi.components.impl.ServiceManagerImpl",
"duration": 37,
"start": 3786,
"end": 3823
},
{
"name": "com.intellij.openapi.util.registry.RegistryState",
"duration": 101,
"start": 3901,
"end": 4003
},
{
"name": "com.intellij.internal.statistic.updater.StatisticsJobsScheduler",
"duration": 15,
"start": 4003,
"end": 4019
},
{
"name": "com.intellij.configurationStore.StoreAwareProjectManager",
"duration": 124,
"start": 4019,
"end": 4143
},
{
"name": "com.intellij.openapi.vfs.PlatformVirtualFileManager",
"duration": 121,
"start": 4019,
"end": 4140
},
{
"name": "com.intellij.openapi.vfs.impl.local.LocalFileSystemImpl",
"duration": 101,
"start": 4022,
"end": 4124
},
{
"name": "com.intellij.openapi.vfs.newvfs.persistent.PersistentFSImpl",
"duration": 95,
"start": 4022,
"end": 4118
},
{
"name": "com.intellij.openapi.fileTypes.impl.FileTypeManagerImpl",
"duration": 480,
"start": 4143,
"end": 4624
},
{
"name": "com.intellij.openapi.editor.impl.EditorFactoryImpl",
"duration": 314,
"start": 4630,
"end": 4944
},
{
"name": "com.intellij.openapi.actionSystem.impl.ActionManagerImpl",
"duration": 300,
"start": 4631,
"end": 4932
},
{
"name": "com.intellij.openapi.keymap.impl.KeymapManagerImpl",
"duration": 16,
"start": 4631,
"end": 4647
},
{
"name": "com.intellij.history.integration.LocalHistoryImpl",
"duration": 14,
"start": 4944,
"end": 4959
},
{
"name": "com.intellij.ide.ui.laf.LafManagerImpl",
"duration": 218,
"start": 4973,
"end": 5191
},
{
"name": "com.intellij.util.net.ssl.CertificateManager",
"duration": 66,
"start": 5218,
"end": 5285
},
{
"name": "com.intellij.openapi.util.registry.RegistryExtensionCollector",
"duration": 13,
"start": 5291,
"end": 5305
},
{
"name": "com.intellij.openapi.wm.impl.FocusManagerImpl",
"duration": 20,
"start": 5305,
"end": 5326
},
{
"name": "com.intellij.openapi.wm.impl.WindowManagerImpl",
"duration": 15,
"start": 5307,
"end": 5323
},
{
"name": "com.intellij.ide.IdeTooltipManager",
"duration": 16,
"start": 5326,
"end": 5342
},
{
"name": "com.intellij.ide.MacOSApplicationProvider",
"duration": 22,
"start": 5354,
"end": 5376
},
{
"name": "com.intellij.openapi.updateSettings.impl.UpdateCheckerComponent",
"duration": 54,
"start": 5376,
"end": 5431
},
{
"name": "com.intellij.util.indexing.FileBasedIndexImpl",
"duration": 550,
"start": 5431,
"end": 5982
},
{
"name": "com.intellij.psi.stubs.SerializationManagerImpl",
"duration": 11,
"start": 5982,
"end": 5993
},
{
"name": "com.intellij.psi.stubs.StubIndexImpl",
"duration": 95,
"start": 5983,
"end": 6078
},
{
"name": "com.intellij.openapi.actionSystem.ex.QuickListsManager",
"duration": 15,
"start": 6083,
"end": 6098
},
{
"name": "com.intellij.execution.ExecutorRegistryImpl",
"duration": 21,
"start": 6108,
"end": 6130
},
{
"name": "com.intellij.util.xml.impl.JavaDomApplicationComponent",
"duration": 11,
"start": 6131,
"end": 6143
},
{
"name": "com.intellij.openapi.projectRoots.impl.DefaultJdkConfigurator",
"duration": 84,
"start": 6144,
"end": 6228
},
{
"name": "org.intellij.lang.xpath.xslt.impl.XsltConfigImpl",
"duration": 11,
"start": 6238,
"end": 6249
},
{
"name": "com.intellij.stats.completion.CompletionTrackerInitializer",
"duration": 10,
"start": 6257,
"end": 6268
},
{
"name": "com.intellij.stats.personalization.impl.ApplicationUserFactorStorage",
"duration": 15,
"start": 6268,
"end": 6283
},
{
"name": "com.intellij.completion.FeatureManagerImpl",
"duration": 13,
"start": 6283,
"end": 6297
},
{
"name": "com.jetbrains.cidr.lang.dfa.contextSensitive.OCSourceGliderComponent",
"duration": 149,
"start": 6298,
"end": 6447
},
{
"name": "org.jetbrains.android.AndroidPlugin",
"duration": 18,
"start": 6458,
"end": 6477
},
{
"name": "org.jetbrains.plugins.ruby.gem.GemManager",
"duration": 10,
"start": 6511,
"end": 6522
}
],
"appOptionsTopHitProviders": [
{
"name": "com.intellij.codeInsight.intention.impl.config.IntentionsOptionsTopHitProvider",
"duration": 1261,
"start": 6633,
"end": 7894
},
{
"name": "com.intellij.codeInsight.template.impl.LiveTemplatesOptionsTopHitProvider",
"duration": 311,
"start": 7894,
"end": 8206
}
],
"preloadActivities": [
{
"name": "com.intellij.ide.ui.OptionsTopHitProvider$Activity",
"duration": 1605,
"start": 6601,
"end": 8206
}
],
"projectComponents": [
{
"name": "com.intellij.openapi.vfs.encoding.EncodingProjectManagerImpl",
"duration": 22,
"start": 8343,
"end": 8366
},
{
"name": "com.intellij.openapi.wm.impl.ToolWindowManagerImpl",
"duration": 20,
"start": 8367,
"end": 8387
},
{
"name": "com.intellij.openapi.roots.impl.ProjectRootManagerComponent",
"duration": 14,
"start": 8388,
"end": 8402
},
{
"name": "com.intellij.psi.impl.PsiManagerImpl",
"duration": 20,
"start": 8402,
"end": 8423
},
{
"name": "com.intellij.openapi.module.impl.ModuleManagerComponent",
"duration": 39,
"start": 8433,
"end": 8473
},
{
"name": "com.intellij.openapi.fileEditor.impl.PsiAwareFileEditorManagerImpl",
"duration": 21,
"start": 8474,
"end": 8495
},
{
"name": "com.intellij.codeInsight.daemon.impl.DaemonCodeAnalyzerImpl",
"duration": 16,
"start": 8502,
"end": 8519
},
{
"name": "com.intellij.codeInsight.daemon.impl.DaemonListeners",
"duration": 200,
"start": 8519,
"end": 8720
},
{
"name": "com.intellij.openapi.vcs.impl.ProjectLevelVcsManagerImpl",
"duration": 21,
"start": 8569,
"end": 8591
},
{
"name": "com.intellij.openapi.vcs.changes.VcsDirtyScopeManagerImpl",
"duration": 109,
"start": 8591,
"end": 8701
},
{
"name": "com.intellij.openapi.vcs.changes.ChangeListManagerImpl",
"duration": 108,
"start": 8591,
"end": 8700
},
{
"name": "com.intellij.openapi.vcs.changes.ChangesViewManager",
"duration": 63,
"start": 8597,
"end": 8660
},
{
"name": "com.intellij.codeInsight.daemon.impl.GeneralHighlightingPassFactory",
"duration": 33,
"start": 8720,
"end": 8754
},
{
"name": "com.intellij.codeInsight.navigation.CtrlMouseHandler",
"duration": 16,
"start": 8759,
"end": 8775
},
{
"name": "com.intellij.openapi.roots.impl.PushedFilePropertiesUpdaterImpl",
"duration": 14,
"start": 8775,
"end": 8790
},
{
"name": "com.intellij.compiler.CompilerConfigurationImpl",
"duration": 10,
"start": 8817,
"end": 8828
},
{
"name": "com.intellij.openapi.vcs.changes.shelf.ShelvedChangesViewManager",
"duration": 29,
"start": 8848,
"end": 8877
},
{
"name": "com.intellij.xdebugger.impl.XDebuggerManagerImpl",
"duration": 98,
"start": 8891,
"end": 8990
},
{
"name": "com.intellij.execution.scratch.JavaScratchCompilationSupport",
"duration": 37,
"start": 9019,
"end": 9056
},
{
"name": "com.intellij.stats.personalization.impl.UserFactorsManagerImpl",
"duration": 22,
"start": 9075,
"end": 9098
},
{
"name": "com.intellij.tasks.impl.TaskManagerImpl",
"duration": 42,
"start": 9098,
"end": 9141
},
{
"name": "com.intellij.uiDesigner.propertyInspector.DesignerToolWindowManager",
"duration": 191,
"start": 9141,
"end": 9332
},
{
"name": "com.intellij.ide.palette.impl.PaletteToolWindowManager",
"duration": 15,
"start": 9332,
"end": 9347
},
{
"name": "com.jetbrains.cidr.lang.symbols.symtable.FileSymbolTablesCache",
"duration": 12,
"start": 9373,
"end": 9385
},
{
"name": "com.jetbrains.cidr.lang.hmap.OCHeaderMapManager",
"duration": 42,
"start": 9386,
"end": 9429
},
{
"name": "com.intellij.jpa.JpaProjectComponent",
"duration": 31,
"start": 9437,
"end": 9469
},
{
"name": "com.android.tools.idea.gradle.project.AndroidGradleProjectComponent",
"duration": 33,
"start": 9490,
"end": 9523
},
{
"name": "com.android.tools.idea.res.PsiProjectListener",
"duration": 12,
"start": 9523,
"end": 9535
},
{
"name": "com.intellij.openapi.roots.impl.ModuleRootManagerComponent",
"duration": 80,
"start": 9606,
"end": 9687
}
],
"projectOptionsTopHitProviders": [
{
"name": "com.intellij.application.options.editor.EditorOptionsTopHitProvider",
"duration": 83,
"start": 11861,
"end": 11944
},
{
"name": "com.intellij.application.options.editor.EditorSmartKeysOptionsTopHitProvider",
"duration": 241,
"start": 11946,
"end": 12187
},
{
"name": "com.intellij.application.options.editor.EditorTabsOptionsTopHitProvider",
"duration": 29,
"start": 12193,
"end": 12223
},
{
"name": "com.intellij.application.options.editor.CodeFoldingOptionsTopHitProvider",
"duration": 69,
"start": 12224,
"end": 12293
},
{
"name": "com.intellij.application.options.editor.AutoImportOptionsTopHitProvider",
"duration": 85,
"start": 12296,
"end": 12381
},
{
"name": "org.intellij.images.options.impl.ImagesOptionsTopHitProvider",
"duration": 80,
"start": 12384,
"end": 12464
},
{
"name": "com.intellij.uiDesigner.GuiDesignerOptionsTopHitProvider",
"duration": 25,
"start": 12472,
"end": 12498
}
],
"totalDurationComputed": 10427,
"totalDurationActual": 13205
}
function main() {
ComponentChartManager
TimelineChartManager
TopHitProviderChart
const container = document.getElementById("visualization")!!
// const chartManager = new ComponentChartManager(container)
// const chartManager = new TimelineChartManager(container)
const chartManager = new TopHitProviderChart(container)
chartManager.render(new DataManager(data))
const global = window as any
global.lastData = data
global.chartManager = chartManager
}
main()
@@ -1,167 +1,17 @@
// 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.
import * as am4core from "@amcharts/amcharts4/core"
import Vue from "vue"
import App from "./App.vue"
import ElementUI from "element-ui"
import router from "./router"
import {store} from "./state"
import locale from "element-ui/lib/locale/lang/en"
import am4themes_animated from "@amcharts/amcharts4/themes/animated"
import {ComponentChartManager, ServiceChartManager, TopHitProviderChart} from "./ItemChartManager"
import {TimelineChartManager} from "./TimeLineChartManager"
import {ChartManager, getButtonElement, getInputElement} from "./core"
import {DataManager, InputData} from "./data"
Vue.use(ElementUI, {locale})
const storageKeyPort = "ijPort"
const storageKeyData = "inputIjFormat"
Vue.config.productionTip = false
function renderDataForCharts(chartManagers: Array<ChartManager>, lastData: DataManager, offset: number = 0) {
for (const chartManager of (offset == 0 ? chartManagers : chartManagers.slice(offset))) {
chartManager.render(lastData)
}
}
function main(): void {
am4core.useTheme(am4themes_animated)
const chartManagers: Array<ChartManager> = []
createTimeLineChart(chartManagers)
createItemChartManagers(chartManagers)
const global = window as any
global.chartManagers = chartManagers
let lastData: DataManager | null = null
new InputFormManager(data => {
lastData = new DataManager(data)
for (const chartManager of chartManagers) {
chartManager.render(lastData)
}
})
if (module != null && module.hot != null) {
module.hot.accept("./ItemChartManager", function() {
chartManagers.length = 1
createItemChartManagers(chartManagers)
if (lastData != null) {
renderDataForCharts(chartManagers, lastData, 1)
}
})
module.hot.accept("./core", function() {
// reload all charts (and only charts, ignore)
chartManagers.length = 0
createTimeLineChart(chartManagers)
createItemChartManagers(chartManagers)
if (lastData != null) {
renderDataForCharts(chartManagers, lastData)
}
})
// https://webpack.js.org/api/hot-module-replacement/#accept-self
module.hot.accept()
}
}
function createTimeLineChart(chartManagers: Array<ChartManager>): void {
chartManagers.push(new TimelineChartManager(document.getElementById("visualization")!!))
}
function createItemChartManagers(chartManagers: Array<ChartManager>): void {
chartManagers.push(new ComponentChartManager(document.getElementById("componentChart")!!))
chartManagers.push(new ServiceChartManager(document.getElementById("serviceChart")!!))
chartManagers.push(new TopHitProviderChart(document.getElementById("optionsTopHitProviderChart")!!))
}
class InputFormManager {
constructor(private dataListener: (data: InputData) => void) {
if (document.readyState === "loading") {
document.addEventListener("DOMContentLoaded", () => {
this.configureElements()
})
}
else {
this.configureElements()
}
}
private callListener(rawData: string) {
this.dataListener(JSON.parse(rawData))
}
private setInput(rawData: string | null) {
if (rawData != null && rawData.length !== 0) {
getInputElement("ijInput").value = rawData
this.callListener(rawData)
}
}
private configureElements(): void {
const inputElement = getInputElement("ijInput")
getPortInputElement().value = localStorage.getItem(storageKeyPort) || "63342"
this.setInput(localStorage.getItem(storageKeyData))
getButtonElement("grabButton").addEventListener("click", () => {
// use parseInt to validate input
let port = getPortInputElement().value
if (port.length === 0) {
port = "63342"
} else if (!/^\d+$/.test(port)) {
throw new Error("Port number value is not numeric")
}
localStorage.setItem(storageKeyPort, port)
this.grabFromRunningInstance(port)
})
getButtonElement("grabDevButton").addEventListener("click", () => {
this.grabFromRunningInstance("63343")
})
inputElement.addEventListener("input", () => {
const rawData = inputElement.value.trim()
localStorage.setItem(storageKeyData, rawData)
this.callListener(rawData)
})
}
private grabFromRunningInstance(port: string) {
const host = `localhost:${port}`
function showError(reason: any) {
alert(`Cannot load data from "${host}": ${reason}`)
}
const controller = new AbortController()
const signal = controller.signal
const timeoutId = setTimeout(() => {
controller.abort()
showError("8 seconds timeout")
}, 8000)
fetch(`http://${host}/api/startUpMeasurement`, {credentials: "omit", signal})
.then(it => it.json())
.then(data => {
clearTimeout(timeoutId)
if (data == null) {
const message = "IntelliJ Platform IDE didn't report startup measurement result"
console.error(message)
alert(message)
return
}
const rawData = JSON.stringify(data, null, 2)
localStorage.setItem(storageKeyData, rawData)
this.setInput(rawData)
})
.catch(e => {
clearTimeout(timeoutId)
console.error(e)
if (!(e instanceof (window as any).AbortError)) {
showError(e)
}
})
}
}
function getPortInputElement() {
return getInputElement("ijPort")
}
main()
new Vue({
render: h => h(App),
store,
router,
}).$mount("#app")
@@ -0,0 +1,38 @@
// 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.
import Vue from "vue"
import Router from "vue-router"
import {Notification} from "element-ui"
import Main from "@/views/Main.vue"
Vue.use(Router)
export default new Router({
routes: [
{
path: "/",
component: Main,
},
// {
// path: "/charts/components",
// name: "Components",
// component: ComponentChart,
// },
// {
// path: "/charts/services",
// name: "Services",
// component: ServiceChart,
// },
// {
// path: "/charts/topHitProviders",
// name: "Options Top Hit Providers",
// component: TopHitOptionPreloaderChart,
// },
{
path: "*",
component: () => {
Notification.error("Path not found")
return Promise.reject(new Error("Path not found"))
},
},
],
})
@@ -0,0 +1,14 @@
// 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.
import Vue, {VNode} from "vue"
declare global {
namespace JSX {
// tslint:disable no-empty-interface
interface Element extends VNode {}
// tslint:disable no-empty-interface
interface ElementClass extends Vue {}
interface IntrinsicElements {
[elem: string]: any
}
}
}
@@ -0,0 +1,15 @@
// 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.
declare module "*.vue" {
import Vue from "vue"
export default Vue
}
// https://github.com/ElemeFE/element/issues/9084
declare module "element-ui/lib/locale/lang/en" {
}
declare module "element-ui/lib/locale" {
let v: any
export default v
}
@@ -0,0 +1,139 @@
// 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.
import Vue from "vue"
import Vuex from "vuex"
import {Module, Mutation, VuexModule} from "vuex-module-decorators"
import VuexPersistence from "vuex-persist"
import {InputData, Item} from "@/data"
const markerNames = ["app initialized callback", "module loading"]
const markerNameToRangeTitle = new Map<string, string>([["app initialized callback", "app initialized"], ["module loading", "project initialized"]])
export const UPDATE_DATE_MUTATION_NAME = "updateData"
export class DataManager {
get data(): InputData {
return this.dataModule.data!!
}
get markerItems(): Array<Item | null> {
return this.dataModule.markerItems
}
constructor(private readonly dataModule: DataModule) {
}
computeGuides(items: Array<Item>): Array<GuideLineDescriptor> {
const rangeItems: Array<Item | null> = new Array(markerNames.length)
rangeItems.fill(null)
let outOfReady: Item | null = null
for (const item of items) {
for (let i = 0; i < this.markerItems.length; i++) {
if (rangeItems[i] == null) {
const markerItem = this.markerItems[i]
if (markerItem != null && item.start >= markerItem.end) {
rangeItems[i] = item
}
}
}
if (outOfReady == null && item.start >= this.data.totalDurationActual) {
outOfReady = item
}
if (outOfReady != null && rangeItems.every(it => it != null)) {
break
}
}
const result: Array<GuideLineDescriptor> = []
for (let i = 0; i < markerNames.length; i++) {
const rangeItem = rangeItems[i]
if (rangeItem != null) {
let rangeLabel = markerNames[i]
const customLabel = markerNameToRangeTitle.get(rangeLabel)
if (customLabel != null) {
rangeLabel = customLabel
}
result.push({item: rangeItem, label: rangeLabel})
}
}
if (outOfReady != null) {
result.push({item: outOfReady, label: "ready"})
}
return result
}
}
export interface GuideLineDescriptor {
readonly item: Item
readonly label: string
}
// name here is important because otherwise getModule from vuex-module-decorators will not work
@Module({name: "data"})
export class DataModule extends VuexModule {
data: InputData | null = null
recentlyUsedIdePort: number = 63342
get markerItems(): Array<Item | null> {
const items = this.data == null ? null : this.data!!.items
if (items == null || items.length === 0) {
return []
}
const markerItems = new Array(markerNames.length)
// JS array is sparse and setting length doesn't pre-fill array
markerItems.fill(null)
itemLoop: for (const item of items) {
for (let i = 0; i < markerNames.length; i++) {
if (markerItems[i] == null && item.name === markerNames[i]) {
markerItems[i] = item
// stop if all items are found
if (markerItems.every(it => it != null)) {
break itemLoop
}
}
}
}
for (let i = 0; i < markerNames.length; i++) {
if (markerItems[i] == null) {
console.error(`Cannot find item for phase "${markerNames[i]}"`)
}
}
return markerItems
}
// don't forget to rename UPDATE_DATE_MUTATION_NAME if method will be renamed
@Mutation
updateData(data: InputData) {
this.data = data
}
@Mutation
updateRecentlyUsedIdePort(value: number) {
this.recentlyUsedIdePort = value
}
}
Vue.use(Vuex)
const vuexLocal = new VuexPersistence({
storage: window.localStorage,
key: "startup-visualizer"
})
export const store = new Vuex.Store({
// strict: process.env.NODE_ENV !== "production",
plugins: [vuexLocal.plugin],
mutations: {
RESTORE_MUTATION: vuexLocal.RESTORE_MUTATION,
},
modules: {
data: DataModule,
}
})
@@ -1,45 +0,0 @@
// 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.
import {computeLevels, TimeLineItem} from "./timeLineChartHelper"
test("sort", () => {
const items = [
{
"name": "default project components creation",
"duration": 13,
"start": 1550854187320,
"end": 1550854187333
},
{
"name": "default project components registration",
"isSubItem": true,
"duration": 0,
"start": 1550854187320,
"end": 1550854187320
},
{
"name": "default project components initialization",
"description": "component count: 24",
"isSubItem": true,
"duration": 13,
"start": 1550854187320,
"end": 1550854187333
},
]
computeLevels(items)
expect(items.map(it => {
return {name: it.name, level: (it as TimeLineItem).level}
})).toMatchObject([
{
"name": "default project components creation",
"level": 0,
},
{
"name": "default project components registration",
"level": 1,
},
{
"name": "default project components initialization",
"level": 1,
},
])
})
@@ -0,0 +1,134 @@
<!-- 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>
<el-row :gutter="16">
<el-col :span="10">
<el-input
type="textarea"
:rows="4"
autocomplete="off"
placeholder="Enter the IntelliJ Platform start-up timeline..."
@change="dataChanged"
@input="inputChanged"
v-model="inputData">
</el-input>
</el-col>
<el-col :span="14">
<el-form :inline="true" size="small">
<el-form-item>
<el-button @click="getFromRunningInstance" :loading="isFetching">Get from running instance</el-button>
</el-form-item>
<el-form-item>
<el-input-number v-model="portNumber" :min="1024" :max="65535"></el-input-number>
</el-form-item>
</el-form>
<el-form :inline="true" size="small">
<el-form-item>
<el-button @click="getFromRunningDevInstance" :loading="isFetchingDev">Get from running instance on port 63343</el-button>
</el-form-item>
</el-form>
</el-col>
</el-row>
</template>
<script lang="ts">
import {Component, Vue} from "vue-property-decorator"
import {getModule} from "vuex-module-decorators"
import {DataModule} from "@/state"
@Component
export default class InputForm extends Vue {
private readonly dataModule = getModule(DataModule, this.$store)
private inputTimerId: number = -1
inputData: string = this.dataModule.data == null ? "" : JSON.stringify(this.dataModule.data, null, 2)
portNumber: number = this.dataModule.recentlyUsedIdePort || 63342
// we can set this flag using reference to button, but "[Vue warn]: Avoid mutating a prop directly...",
// so, it seems that data property it is the only recommended way
isFetching: boolean = false
isFetchingDev: boolean = false
dataChanged() {
this.dataModule.updateData(JSON.parse(this.inputData))
}
inputChanged() {
if (this.inputTimerId !== -1) {
clearTimeout(this.inputTimerId)
}
this.inputTimerId = setTimeout(() => {
this.dataChanged()
this.inputTimerId = -1
}, 500)
}
getFromRunningInstance() {
this.isFetching = true
// portNumber will be here as number, thanks to Vue.js, no need to validate
// if (!/^\d+$/.test(port)) {
// throw new Error("Port number value is not numeric")
// }
this.dataModule.updateRecentlyUsedIdePort(this.portNumber)
this.doGetFromRunningInstance(this.portNumber, () => {
this.isFetching = false
})
}
getFromRunningDevInstance() {
this.isFetchingDev = true
this.doGetFromRunningInstance(63343, () => {
this.isFetchingDev = false
})
}
private doGetFromRunningInstance(port: number, processed: () => void) {
const host = `localhost:${port}`
const showError = (reason: string) => {
this.$notify.error({
title: "Error",
message: `Cannot load data from "${host}": ${reason}`,
})
}
let isCancelledByTimeout = false
const controller = new AbortController()
const signal = controller.signal
const timeoutId = setTimeout(() => {
isCancelledByTimeout = true
controller.abort()
showError("8 seconds timeout")
}, 8000)
fetch(`http://${host}/api/startUpMeasurement`, {credentials: "omit", signal})
.then(it => it.json())
.then(data => {
clearTimeout(timeoutId)
if (data == null) {
const message = "IntelliJ Platform IDE didn't report startup measurement result"
console.error(message)
alert(message)
return
}
processed()
this.dataModule.updateData(data)
this.inputData = JSON.stringify(data, null, 2)
})
.catch(e => {
processed()
clearTimeout(timeoutId)
if (!isCancelledByTimeout) {
showError(e.toString())
}
console.warn(`Cannot load data from "${host}"`, e)
})
}
}
</script>
@@ -0,0 +1,49 @@
<!-- 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>
<InputForm/>
<el-row>
<el-col>
<small>
For timeline no threshold, for other charts threshold is 10ms.
End time equals to last dumb-aware project start-up activity (so, services and others may be out of end time).
</small>
</el-col>
</el-row>
<TimelineChart/>
<TabbedCharts/>
<el-row>
<el-col>
<ul>
<li>
<small>app initialized: end of phase <code>app initialized callback</code>.</small>
</li>
<li>
<small>project initialized: end of phase <code>module loading</code>.</small>
</li>
</ul>
</el-col>
</el-row>
</div>
</template>
<script lang="ts">
import {Component, Vue} from "vue-property-decorator"
import InputForm from "@/views/InputForm.vue"
import TabbedCharts from "@/views/TabbedCharts.vue"
import TimelineChart from "@/views/TimelineChart.vue"
@Component({components: {InputForm, TimelineChart, TabbedCharts}})
export default class Main extends Vue {
}
</script>
<style>
.activityChart {
width: 100%;
height: 380px;
}
</style>
@@ -0,0 +1,53 @@
<!-- 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>
<el-tabs v-model="activeName" @tab-click="navigate">
<!-- use v-once because `charts` is not going to be changed -->
<el-tab-pane v-once v-for="item in charts" :label="item.label" :name="item.name" lazy>
<keep-alive>
<ItemChart :type="item.name"/>
</keep-alive>
</el-tab-pane>
</el-tabs>
</template>
<script lang="ts">
import {Component, Vue, Watch} from "vue-property-decorator"
import ItemChart from "@/charts/ItemChart.vue"
import {ItemChartType} from "@/charts/ItemChartManager"
import {Location} from "vue-router"
interface ItemChartDescriptor {
readonly label: string
readonly name: ItemChartType
}
@Component({components: {ItemChart}})
export default class TabbedCharts extends Vue {
charts: Array<ItemChartDescriptor> = [
{label: "Components", name: "components"},
{label: "Services", name: "services"},
{label: "Options Top Hit Providers", name: "topHitProviders"},
]
activeName: ItemChartType = this.charts[0].name
created() {
this.updateLocation(this.$route)
}
@Watch("$route")
onRouteChanged(location: Location, _oldLocation: Location) {
this.updateLocation(location)
}
private updateLocation(location: Location) {
const tab = location.query == null ? null : location.query.tab
// do not check `location.path === "/"` because if component displayed, so, active
this.activeName = tab == null ? this.charts[0].name : tab as ItemChartType
}
navigate() {
this.$router.push({query: {tab: this.activeName}})
}
}
</script>
@@ -0,0 +1,38 @@
<!-- 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>
</template>
<script lang="ts">
import {Component, Prop, Vue} from "vue-property-decorator"
import {DataManager, DataModule, UPDATE_DATE_MUTATION_NAME} from "@/state"
import {getModule} from "vuex-module-decorators"
import {ChartManager} from "@/core"
import {TimelineChartManager} from "@/charts/TimeLineChartManager"
@Component
export default class TimelineChart extends Vue {
@Prop(String)
type!: string
private readonly dataModule = getModule(DataModule, this.$store)
mounted() {
const chartManager = this.createChartManager()
this.$store.subscribe(mutation => {
if (mutation.type === UPDATE_DATE_MUTATION_NAME) {
chartManager.render(new DataManager(this.dataModule))
}
})
const data = this.dataModule.data
if (data != null) {
chartManager.render(new DataManager(this.dataModule))
}
}
private createChartManager(): ChartManager {
return new TimelineChartManager(this.$refs.chartContainer as HTMLElement)
}
}
</script>
@@ -1,16 +1,42 @@
{
"compilerOptions": {
"strict": true,
"moduleResolution": "node",
"target": "es2016",
"newLine": "LF",
"noEmitOnError": true,
"module": "esnext",
"strict": true,
"noUnusedParameters": true,
"noUnusedLocals": true,
"outDir": "dist",
"sourceMap": true
"newLine": "LF",
"jsx": "preserve",
"importHelpers": true,
"moduleResolution": "node",
"experimentalDecorators": true,
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"sourceMap": true,
"baseUrl": ".",
"types": [
"webpack-env"
],
"paths": {
"@/*": [
"src/*"
]
},
"lib": [
"esnext",
"dom",
"dom.iterable",
"scripthost"
]
},
"include": [
"src/*.ts"
"src/**/*.ts",
"src/**/*.tsx",
"src/**/*.vue",
"tests/**/*.ts",
"tests/**/*.tsx"
],
"exclude": [
"node_modules"
]
}
}
@@ -0,0 +1,14 @@
// 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.
module.exports = {
configureWebpack: {
devtool: "source-map",
},
integrity: true,
chainWebpack: config => {
config.externals({
"@amcharts/amcharts4/core": "am4core",
"@amcharts/amcharts4/charts": "am4charts",
"@amcharts/amcharts4/themes/animated": "am4themes_animated",
})
},
}
@@ -1,87 +0,0 @@
// 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.
const path = require("path")
const webpack = require("webpack")
const CleanWebpackPlugin = require("clean-webpack-plugin")
const HtmlWebpackPlugin = require("html-webpack-plugin")
const SriPlugin = require("webpack-subresource-integrity")
const CopyPlugin = require("copy-webpack-plugin")
const ForkTsCheckerWebpackPlugin = require("fork-ts-checker-webpack-plugin")
const ForkTsCheckerNotifierWebpackPlugin = require("fork-ts-checker-notifier-webpack-plugin")
function configurePlugins(isProduction) {
const plugins = [
new CleanWebpackPlugin(["dist/*.js", "dist/*.js.map"]),
new HtmlWebpackPlugin({
template: path.join(__dirname, "index.template.html"),
}),
new SriPlugin({
hashFuncNames: ["sha384"],
enabled: isProduction,
}),
new CopyPlugin([
{from: path.join(__dirname, "main.css")},
]),
]
if (!isProduction) {
plugins.push(new ForkTsCheckerWebpackPlugin({
useTypescriptIncrementalApi: true,
}),)
plugins.push(new ForkTsCheckerNotifierWebpackPlugin({
title: "TypeScript",
excludeWarnings: false,
skipFirstNotification: true,
}))
}
return plugins
}
module.exports = module.exports = function (env, argv) {
const isProduction = argv.mode === "production"
const isMinimizeDisabledExplicitly = argv["optimize-minimize"] === false
const configuration = {
entry: path.join(__dirname, "src/main.ts"),
devtool: "source-map",
module: {
rules: [
{
test: /\.tsx?$/,
use: [
{loader: "ts-loader", options: {transpileOnly: !isProduction}}
],
exclude: /node_modules/
}
],
},
resolve: {
extensions: [".tsx", ".ts", ".js"],
},
output: {
filename: isProduction && !isMinimizeDisabledExplicitly ? "[name].[contenthash].js" : "[name].js",
path: path.join(__dirname, "dist"),
crossOriginLoading: "anonymous",
},
externals: {
"@amcharts/amcharts4/core": "am4core",
"@amcharts/amcharts4/charts": "am4charts",
"@amcharts/amcharts4/themes/animated": "am4themes_animated",
},
devServer: {
hot: true,
},
plugins: configurePlugins(isProduction),
}
// --optimize-minimize=false doesn't work otherwise
if (isMinimizeDisabledExplicitly) {
configuration.optimization = {
minimize: false
}
}
return configuration
}
File diff suppressed because it is too large Load Diff