/** * Copyright 2013 the original author or authors. * * Licensed under the Apache License, Version 2.0 the "License"; * you may not use this file except in compliance with the License. * You may obtain a copy of the License at * * http://www.apache.org/licenses/LICENSE-2.0 * Unless required by applicable law or agreed to in writing, software * distributed under the License is distributed on an "AS IS" BASIS, * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * See the License for the specific language governing permissions and * limitations under the License. **/ package io.neba.core.resourcemodels.metadata; import org.apache.felix.webconsole.AbstractWebConsolePlugin; import org.apache.sling.commons.json.JSONArray; import org.apache.sling.commons.json.JSONObject; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.net.URL; import java.util.LinkedHashMap; import java.util.Map; import static java.lang.Math.round; import static org.apache.commons.collections.CollectionUtils.find; import static org.apache.commons.lang.StringUtils.*; /** * Provides a RESTFul JSON API for {@link io.neba.api.annotations.ResourceModel} metadata, * i.e. the metadata collected at both registration and runtime. The metadata - in particular the * {@link ResourceModelStatistics} - is visualized by this console plugin on the client-side using D3.js. * * @author Olaf Otto */ @Service public class ModelStatisticsConsolePlugin extends AbstractWebConsolePlugin { public static final String LABEL = "modelstatistics"; private static final long serialVersionUID = -8676958166611686979L; private static final String STATISTICS_API_PATH = "/api/statistics"; private static final String RESET_API_PATH = "/api/reset"; @Autowired private ResourceModelMetaDataRegistrar modelMetaDataRegistrar; @SuppressWarnings("unused") public String getCategory() { return "NEBA"; } @Override public String getLabel() { return LABEL; } @Override public String getTitle() { return "Model statistics"; } @Override protected void doGet(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { String suffix = substringAfter(req.getRequestURI(), req.getServletPath() + "/" + getLabel()); if (!isBlank(suffix) && suffix.startsWith(STATISTICS_API_PATH)) { setNoCacheHeaders(res); getModelMetadata(suffix.substring(STATISTICS_API_PATH.length()), res); return; } if (!isBlank(suffix) && suffix.startsWith(RESET_API_PATH)) { setNoCacheHeaders(res); resetStatistics(res); return; } super.doGet(req, res); } private void setNoCacheHeaders(HttpServletResponse res) { res.setHeader("Expires", "Sat, 6 May 1970 12:00:00 GMT"); res.setHeader("Cache-Control", "no-store, no-cache, must-revalidate"); res.addHeader("Cache-Control", "post-check=0, pre-check=0"); res.setHeader("Pragma", "no-cache"); } private void resetStatistics(HttpServletResponse res) { for (ResourceModelMetaData metaData : this.modelMetaDataRegistrar.get()) { metaData.getStatistics().reset(); } prepareJsonResponse(res); try { res.getWriter().write("{\"success\": true}"); } catch (IOException e) { throw new RuntimeException("Unable to write success message after statistics cleared.", e); } } private void getModelMetadata(String typePath, HttpServletResponse res) { if (typePath.isEmpty()) { provideStatisticsOfAllModels(res); } else { String typeName = typePath.substring(1); provideStatisticsOfModel(typeName, res); } } private void provideStatisticsOfModel(final String typeName, HttpServletResponse res) { ResourceModelMetaData metaData = (ResourceModelMetaData) find( this.modelMetaDataRegistrar.get(), object -> ((ResourceModelMetaData) object).getTypeName().equals(typeName) ); if (metaData != null) { Map<String, Object> data = data(metaData); ResourceModelStatistics statistics = metaData.getStatistics(); int[] mappingDurationFrequencies = statistics.getMappingDurationFrequencies(); int[] intervalBoundaries = statistics.getMappingDurationIntervalBoundaries(); try { JSONObject json = new JSONObject(data); JSONObject durationFrequencies = new JSONObject(); int leftBoundary = 0; for (int i = 0; i < mappingDurationFrequencies.length; ++i) { durationFrequencies.put("[" + leftBoundary + ", " + intervalBoundaries[i] + ")", mappingDurationFrequencies[i]); leftBoundary = intervalBoundaries[i]; } json.put("mappingDurationFrequencies", durationFrequencies); prepareJsonResponse(res); json.write(res.getWriter()); } catch (Exception e) { throw new RuntimeException("Unable to write the resource model JSON data.", e); } } } private void prepareJsonResponse(HttpServletResponse res) { res.setCharacterEncoding("UTF-8"); res.setContentType("application/json; charset=UTF-8"); } private void provideStatisticsOfAllModels(HttpServletResponse res) { JSONArray array = new JSONArray(); for (ResourceModelMetaData metaData : this.modelMetaDataRegistrar.get()) { Map<String, Object> data = data(metaData); array.put(data); } try { prepareJsonResponse(res); array.write(res.getWriter()); } catch (Exception e) { throw new RuntimeException("Unable to write the resource model JSON data.", e); } } private Map<String, Object> data(ResourceModelMetaData metaData) { ResourceModelStatistics statistics = metaData.getStatistics(); // Map<String, Object> data = new LinkedHashMap<>(); int lazyFields = 0, greedyFields = 0; for (MappedFieldMetaData field : metaData.getMappableFields()) { boolean isLazyLoadedField = field.isOptional() || field.isChildrenAnnotationPresent() || field.isReference() && field.isInstantiableCollectionType(); if (isLazyLoadedField) { ++lazyFields; } else { ++greedyFields; } } data.put("type", metaData.getTypeName()); data.put("since", statistics.getSince()); data.put("mappableFields", metaData.getMappableFields().length); data.put("lazyFields", lazyFields); data.put("greedyFields", greedyFields); data.put("instantiations", statistics.getInstantiations()); data.put("mappings", statistics.getNumberOfMappings()); data.put("averageMappingDuration", statistics.getAverageMappingDuration()); data.put("totalMappingDuration", statistics.getTotalMappingDuration()); data.put("maximumMappingDuration", statistics.getMaximumMappingDuration()); data.put("minimumMappingDuration", statistics.getMinimumMappingDuration()); data.put("mappingDurationMedian", statistics.getMappingDurationMedian()); data.put("cacheHits", statistics.getCacheHits()); return data; } @Override protected void renderContent(HttpServletRequest req, HttpServletResponse res) throws ServletException, IOException { writeHeadnavigation(res); writeBody(res); } public URL getResource(String path) { URL url = null; String internalPath = substringAfter(path, "/" + getLabel()); if (startsWith(internalPath, "/static/")) { url = getClass().getResource("/META-INF/consoleplugin/modelstatistics" + internalPath); } return url; } private void writeHeadnavigation(HttpServletResponse response) throws IOException { int numberOfModelsWithInstantiations = 0; double highestAverageMappingDuration = 0D; int highestNumberOfFields = 0; String nameOfModelWithHighestAverageMappingDuration = ""; String nameOfModelWithGreatestNumberOfFields = ""; for (ResourceModelMetaData metaData : this.modelMetaDataRegistrar.get()) { ResourceModelStatistics statistics = metaData.getStatistics(); if (statistics.getInstantiations() != 0) { ++numberOfModelsWithInstantiations; double averageMappingDuration = statistics.getAverageMappingDuration(); if (averageMappingDuration > highestAverageMappingDuration) { highestAverageMappingDuration = averageMappingDuration; nameOfModelWithHighestAverageMappingDuration = metaData.getTypeName(); } int numberOfMappableFields = metaData.getMappableFields().length; if (numberOfMappableFields > highestNumberOfFields) { highestNumberOfFields = numberOfMappableFields; nameOfModelWithGreatestNumberOfFields = metaData.getTypeName(); } } } String template = readTemplateFile("/META-INF/consoleplugin/modelstatistics/templates/head.html"); response.getWriter().printf(template, numberOfModelsWithInstantiations, round(highestAverageMappingDuration), nameOfModelWithHighestAverageMappingDuration, highestNumberOfFields, nameOfModelWithGreatestNumberOfFields); } private void writeBody(HttpServletResponse response) throws IOException { String template = readTemplateFile("/META-INF/consoleplugin/modelstatistics/templates/plots.html"); response.getWriter().print(template); } }