diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java index e1218771f..ba824a548 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/AbstractInjectedIntoHoverProvider.java @@ -10,6 +10,7 @@ *******************************************************************************/ package org.springframework.ide.vscode.boot.java.livehover; +import java.time.Duration; import java.util.Collection; import java.util.Collections; import java.util.List; @@ -185,6 +186,16 @@ public abstract class AbstractInjectedIntoHoverProvider implements HoverProvider if (!wiredBeans.isEmpty()) { AutowiredHoverProvider.createHoverContentForBeans(sourceLinks, project, hover, wiredBeans); } + + if (liveData.getStartup() != null) { + Duration instanciationTime = liveData.getStartup().getBeanInstanciationTime(definedBean.getId()); + if (instanciationTime != null) { + hover.append("Instanciation Time: "); + hover.append(instanciationTime.toMillis()); + hover.append("ms"); + hover.append("\n\n"); + } + } hover.append("Bean id: `"); hover.append(definedBean.getId()); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnector.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnector.java index 2d7d015ba..be54b18a9 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnector.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnector.java @@ -18,7 +18,7 @@ public interface SpringProcessConnector { String getProcessKey(); void connect() throws Exception; - SpringProcessLiveData refresh() throws Exception; + SpringProcessLiveData refresh(SpringProcessLiveData currentData) throws Exception; void disconnect() throws Exception; void addConnectorChangeListener(SpringProcessConnectionChangeListener listener); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverJMX.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverJMX.java index b7c5920ad..94fe8a7d1 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverJMX.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorOverJMX.java @@ -107,7 +107,7 @@ public class SpringProcessConnectorOverJMX implements SpringProcessConnector { } @Override - public SpringProcessLiveData refresh() throws Exception { + public SpringProcessLiveData refresh(SpringProcessLiveData currentData) throws Exception { log.info("try to open JMX connection to: " + jmxURL); if (jmxConnection != null) { @@ -119,7 +119,7 @@ public class SpringProcessConnectorOverJMX implements SpringProcessConnector { } log.info("retrieve live data from: " + jmxURL); - SpringProcessLiveData liveData = springJMXConnector.retrieveLiveData(jmxConnection, processID, processName, urlScheme, host, null, port); + SpringProcessLiveData liveData = springJMXConnector.retrieveLiveData(jmxConnection, processID, processName, urlScheme, host, null, port, currentData); if (this.processID == null) { this.processID = liveData.getProcessID(); diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java index 5e350a84b..9fccf5e22 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessConnectorService.java @@ -210,7 +210,7 @@ public class SpringProcessConnectorService { try { progressTask.progressEvent(progressMessage); - SpringProcessLiveData newLiveData = connector.refresh(); + SpringProcessLiveData newLiveData = connector.refresh(this.liveDataProvider.getCurrent(processKey)); if (newLiveData != null) { if (!this.liveDataProvider.add(processKey, newLiveData)) { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveData.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveData.java index 7d56ca306..97e40b35d 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveData.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveData.java @@ -29,11 +29,12 @@ public class SpringProcessLiveData { private final LiveConditional[] conditionals; private final LiveProperties properties; private final LiveMetricsModel metrics; + private StartupModel startup; public SpringProcessLiveData(String processName, String processID, String contextPath, String urlScheme, String port, String host, LiveBeansModel beansModel, String[] activeProfiles, LiveRequestMapping[] requestMappings, LiveConditional[] conditionals, LiveProperties properties, - LiveMetricsModel metrics) { + LiveMetricsModel metrics, StartupModel startup) { super(); this.processName = processName; this.processID = processID; @@ -47,6 +48,8 @@ public class SpringProcessLiveData { this.conditionals = conditionals; this.properties = properties; this.metrics = metrics; + this.startup = startup; + } public String getProcessName() { @@ -97,4 +100,8 @@ public class SpringProcessLiveData { return this.metrics; } + public StartupModel getStartup() { + return startup; + } + } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java index adf6a1fe1..9b624db95 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataExtractorOverJMX.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.Collections; import java.util.Iterator; import java.util.List; +import java.util.Map; import java.util.Map.Entry; import java.util.Properties; import java.util.Set; @@ -61,9 +62,10 @@ public class SpringProcessLiveDataExtractorOverJMX { * @param host should always be != null * @param contextPath if null, will be determined searching existing mbeans for that information (for local processes) * @param port if null, will be determined searching existing mbeans for that information (for local processes) + * @param currentData currently stored live data */ public SpringProcessLiveData retrieveLiveData(JMXConnector jmxConnector, String processID, String processName, - String urlScheme, String host, String contextPath, String port) { + String urlScheme, String host, String contextPath, String port, SpringProcessLiveData currentData) { try { MBeanServerConnection connection = jmxConnector.getMBeanServerConnection(); @@ -91,6 +93,7 @@ public class SpringProcessLiveDataExtractorOverJMX { LiveRequestMapping[] requestMappings = getRequestMappings(connection, domain); LiveBeansModel beans = getBeans(connection, domain); LiveMetricsModel metrics = getMetrics(connection, domain); + StartupModel startup = getStartup(connection, domain, currentData == null ? null : currentData.getStartup()); if (contextPath == null) { contextPath = getContextPath(connection, domain, environment); @@ -112,7 +115,8 @@ public class SpringProcessLiveDataExtractorOverJMX { requestMappings, conditionals, properties, - metrics); + metrics, + startup); } catch (Exception e) { log.error("error reading live data from: " + processID + " - " + processName, e); @@ -159,6 +163,21 @@ public class SpringProcessLiveDataExtractorOverJMX { }; } + + private StartupModel getStartup(MBeanServerConnection connection, String domain, StartupModel currentStartup) { + if (currentStartup != null) { + return currentStartup; + } + try { + Map result = (Map) getActuatorDataFromOperation(connection, getObjectName(domain, "type=Endpoint,name=Startup"), "startup"); + if (result != null) { + return StartupModel.parse(result); + } + } catch (Exception e) { + log.error("", e); + } + return null; + } public String getProcessID(MBeanServerConnection connection) { try { diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataProvider.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataProvider.java index 8c415bdf5..a24d40c28 100644 --- a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataProvider.java +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/SpringProcessLiveDataProvider.java @@ -78,5 +78,9 @@ public class SpringProcessLiveDataProvider { listener.liveDataChanged(event); } } + + SpringProcessLiveData getCurrent(String processKey) { + return this.liveData.get(processKey); + } } diff --git a/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/StartupModel.java b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/StartupModel.java new file mode 100644 index 000000000..3c3429081 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/main/java/org/springframework/ide/vscode/boot/java/livehover/v2/StartupModel.java @@ -0,0 +1,151 @@ +package org.springframework.ide.vscode.boot.java.livehover.v2; + +import java.time.Duration; +import java.util.Date; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import com.google.gson.JsonDeserializer; +import com.google.gson.reflect.TypeToken; + +public class StartupModel { + + private static final String BEAN_INSTANCIATION_EVENT = "spring.beans.instantiate"; + + private static final Gson gson = new GsonBuilder() + .registerTypeAdapter(Duration.class, (JsonDeserializer) (json, typeOfT, context) -> Duration.parse(json.getAsString())) + .create(); + + public static class StartupEvent { + private Date startTime; + private Date endTime; + private Duration duration; + private StartupStep startupStep; + public Date getStartTime() { + return startTime; + } + public void setStartTime(Date startTime) { + this.startTime = startTime; + } + public Date getEndTime() { + return endTime; + } + public void setEndTime(Date endTime) { + this.endTime = endTime; + } + public Duration getDuration() { + return duration; + } + public void setDuration(Duration duration) { + this.duration = duration; + } + public StartupStep getStartupStep() { + return startupStep; + } + public void setStartupStep(StartupStep startupStep) { + this.startupStep = startupStep; + } + } + + public static class StartupStep { + private String name; + private int id; + private int parentId; + private PropertyValuePair[] tags; + public String getName() { + return name; + } + public void setName(String name) { + this.name = name; + } + public int getId() { + return id; + } + public void setId(int id) { + this.id = id; + } + public int getParentId() { + return parentId; + } + public void setParentId(int parentId) { + this.parentId = parentId; + } + public PropertyValuePair[] getTags() { + return tags; + } + public void setTags(PropertyValuePair[] tags) { + this.tags = tags; + } + String findPropertyValue(String key) { + for (PropertyValuePair pair : tags) { + if (key.equals(pair.getKey())) { + return pair.getValue(); + } + } + return null; + } + } + + public static class PropertyValuePair { + private String key; + private String value; + public String getKey() { + return key; + } + public void setKey(String key) { + this.key = key; + } + public String getValue() { + return value; + } + public void setValue(String value) { + this.value = value; + } + } + + public static StartupModel parse(Map mapContent) { + Object timeline = mapContent.get("timeline"); + if (timeline instanceof Map) { + Object events = ((Map) timeline).get("events"); + if (events instanceof List) { + List fromJson = gson.fromJson(gson.toJson(events), new TypeToken>() {}.getType()); + return new StartupModel(fromJson); + } + } + return null; + } + + private List startupEvents; + + private Map beanInstanciationTimes; + + public StartupModel(List startupEvents) { + this.startupEvents = startupEvents; + beanInstanciationTimes = createbeanInstanciationTimes(); + } + + private Map createbeanInstanciationTimes() { + Map beanInstanciationTimes = new HashMap<>(); + for (StartupEvent event : startupEvents) { + if (event.getStartupStep() != null && BEAN_INSTANCIATION_EVENT.equals(event.getStartupStep().getName())) { + String beanId = event.getStartupStep().findPropertyValue("beanName"); + if (beanId != null) { + beanInstanciationTimes.put(beanId, event.getDuration()); + } + } + } + return beanInstanciationTimes; + } + + public Duration getBeanInstanciationTime(String beanId) { + return beanInstanciationTimes.get(beanId); + } + + public List getStartupEvents() { + return startupEvents; + } + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/metrics/test/StartupMetricsTest.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/metrics/test/StartupMetricsTest.java new file mode 100644 index 000000000..382918bb2 --- /dev/null +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/boot/java/metrics/test/StartupMetricsTest.java @@ -0,0 +1,5 @@ +package org.springframework.ide.vscode.boot.java.metrics.test; + +public class StartupMetricsTest { + +} diff --git a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/SpringProcessLiveDataBuilder.java b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/SpringProcessLiveDataBuilder.java index 0c0179ec1..4e9565c98 100644 --- a/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/SpringProcessLiveDataBuilder.java +++ b/headless-services/spring-boot-language-server/src/test/java/org/springframework/ide/vscode/project/harness/SpringProcessLiveDataBuilder.java @@ -23,6 +23,7 @@ import org.springframework.ide.vscode.boot.java.livehover.v2.LiveMetricsModel; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveProperties; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveRequestMapping; import org.springframework.ide.vscode.boot.java.livehover.v2.LiveRequestMappingBoot1xRequestMapping; +import org.springframework.ide.vscode.boot.java.livehover.v2.StartupModel; import org.springframework.ide.vscode.boot.java.livehover.v2.SpringProcessLiveData; /** @@ -44,6 +45,7 @@ public class SpringProcessLiveDataBuilder { private LiveConditional[] conditionals; private LiveProperties properties; private LiveMetricsModel metrics; + private StartupModel startup; public SpringProcessLiveDataBuilder processName(String processName) { this.processName = processName; @@ -129,8 +131,13 @@ public class SpringProcessLiveDataBuilder { return this; } + public SpringProcessLiveDataBuilder liveStartup(StartupModel startup) { + this.startup = startup; + return this; + } + public SpringProcessLiveData build() { - return new SpringProcessLiveData(processName, processID, contextPath, urlScheme, port, host, beansModel, activeProfiles, requestMappings, conditionals, properties, metrics); + return new SpringProcessLiveData(processName, processID, contextPath, urlScheme, port, host, beansModel, activeProfiles, requestMappings, conditionals, properties, metrics, startup); } }