/* * Copyright 2008-2017 by Emeric Vernat * * This file is part of Java Melody. * * 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 net.bull.javamelody; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.lowagie.text.Document; import com.lowagie.text.DocumentException; import com.lowagie.text.Element; import com.lowagie.text.Font; import com.lowagie.text.Phrase; import com.lowagie.text.pdf.PdfPCell; import com.lowagie.text.pdf.PdfPTable; /** * Partie du rapport pdf pour les dépendances en exécution. * @author Emeric Vernat */ class PdfRuntimeDependenciesReport extends PdfAbstractReport { private final Counter counter; private final Font warningCellFont = PdfFonts.WARNING_CELL.getFont(); private final Font severeCellFont = PdfFonts.SEVERE_CELL.getFont(); private final Font normalFont = PdfFonts.NORMAL.getFont(); private final Font cellFont = PdfFonts.TABLE_CELL.getFont(); private final Font boldCellFont = PdfFonts.BOLD_CELL.getFont(); private PdfPTable currentTable; private List<String> calledBeans; private double standardDeviation; PdfRuntimeDependenciesReport(Counter counter, Document document) { super(document); assert counter != null; this.counter = counter; } /** * Retourne les dépendances entre classes selon les requêtes de counter, * selon les méthodes réellement exécutés et monitorées sur la période sélectionnée.<br/> * Le niveau de dépendance entre chaque classe est indiqué par le nombre de méthodes * différentes appelées depuis une classe vers une autre classe * (et non par le nombre d'exécutions et non plus par le nombre d'appels de méthodes).<br/> * Une classe ne peut pas avoir de dépendance avec elle-même.<br/> * Exemple:<br/><pre> * ex : * public class A { * @Inject * private B b; * * public void a() { * b.b1(); * for (int i = 0; i < 100; i++) { * b.b2(); * } * if (false) { * b.b3(); * } * } * } * </pre> * Si seulement les classes A et B sont monitorées et si la méthode "a" est appelée au moins * une fois, alors les "runtimeDependencies" contiendront une dépendance de A vers B. * Cette dépendance sera de 2 méthodes (une pour "b1" et une pour "b2"). * @return Map */ Map<String, Map<String, Integer>> getRuntimeDependencies() { final Map<String, Set<CounterRequest>> methodsCalledByCallerBeans = getMethodsCalledByCallerBeans(); final Map<String, Map<String, Integer>> result = new HashMap<String, Map<String, Integer>>(); for (final Map.Entry<String, Set<CounterRequest>> entry : methodsCalledByCallerBeans .entrySet()) { final String callerBean = entry.getKey(); final Set<CounterRequest> childRequests = entry.getValue(); final Map<String, Integer> nbMethodsCalledByCalledBean = new HashMap<String, Integer>(); for (final CounterRequest childRequest : childRequests) { final String calledBean = getClassNameFromRequest(childRequest); if (callerBean.equals(calledBean)) { // si le bean est lui même, il peut s'appeler autant qu'il veut // ce n'est pas une dépendance continue; } Integer nbOfMethodCalls = nbMethodsCalledByCalledBean.get(calledBean); if (nbOfMethodCalls == null) { nbOfMethodCalls = 1; } else { // on compte le nombre de méthodes appelées et non le nombre d'exécutions nbOfMethodCalls = nbOfMethodCalls + 1; } nbMethodsCalledByCalledBean.put(calledBean, nbOfMethodCalls); } // methodsCalled peut être vide soit parce que childRequestsByBeans est vide // (il n'y a pas d'appels de méthodes pour ce bean, que des requêtes sql par exemple), // soit parce qu'il n'y a que des appels de méthodes vers le bean lui-même if (!nbMethodsCalledByCalledBean.isEmpty()) { result.put(callerBean, nbMethodsCalledByCalledBean); } } return result; } private Map<String, Set<CounterRequest>> getMethodsCalledByCallerBeans() { assert counter.isBusinessFacadeCounter(); final List<CounterRequest> requests = counter.getRequests(); final Map<String, CounterRequest> requestsById = new HashMap<String, CounterRequest>(); for (final CounterRequest request : requests) { requestsById.put(request.getId(), request); } // on compte le nombre de méthodes différentes appelées depuis un bean vers un autre bean // et non le nombre d'exécutions et non plus le nombre d'appels de méthodes final Map<String, Set<CounterRequest>> methodsCalledByCallerBeans = new HashMap<String, Set<CounterRequest>>(); for (final CounterRequest request : requests) { final Set<String> childRequestIds = request.getChildRequestsExecutionsByRequestId() .keySet(); if (childRequestIds.isEmpty()) { continue; } final String callerBean = getClassNameFromRequest(request); // c'est un Set donc on ne compte une méthode 'a' appelée depuis une classe qu'une seule fois // même si elle est appelée deux fois depuis la même classe // ou même à un seul endroit mais avec des exécutions dans une boucle Set<CounterRequest> childRequests = methodsCalledByCallerBeans.get(callerBean); if (childRequests == null) { childRequests = new HashSet<CounterRequest>(); methodsCalledByCallerBeans.put(callerBean, childRequests); } for (final String childRequestId : childRequestIds) { // on ne regarde que les requêtes du même counter // (pas sql, et pas guice si on est dans spring) if (counter.isRequestIdFromThisCounter(childRequestId)) { final CounterRequest childRequest = requestsById.get(childRequestId); // il peut y avoir une non synchronisation temporaire entre la liste des requêtes // et les requêtes filles if (childRequest != null) { childRequests.add(childRequest); } } } } return methodsCalledByCallerBeans; } private static String getClassNameFromRequest(CounterRequest request) { final int lastIndexOf = request.getName().lastIndexOf('.'); if (lastIndexOf != -1) { return request.getName().substring(0, lastIndexOf); } return request.getName(); } private static double getStandardDeviation( Map<String, Map<String, Integer>> runtimeDependencies) { final List<Integer> values = new ArrayList<Integer>(); int sum = 0; for (final Map<String, Integer> beanDependencies : runtimeDependencies.values()) { for (final Integer value : beanDependencies.values()) { values.add(value); sum += value; } } final int mean = sum / values.size(); int square = 0; for (final Integer value : values) { final int diff = value - mean; square += diff * diff; } return Math.sqrt((double) square / values.size()); } private List<String> getCalledBeans(Map<String, Map<String, Integer>> runtimeDependencies) { final Set<String> calledBeansSet = new HashSet<String>(); for (final Map<String, Integer> values : runtimeDependencies.values()) { calledBeansSet.addAll(values.keySet()); } final List<String> result = new ArrayList<String>(calledBeansSet); Collections.sort(result); return result; } @Override void toPdf() throws DocumentException { final Map<String, Map<String, Integer>> runtimeDependencies = getRuntimeDependencies(); if (runtimeDependencies.isEmpty()) { addToDocument(new Phrase(getString("Aucune_dependance"), normalFont)); return; } addToDocument(new Phrase(getString("runtime_dependencies_desc"), normalFont)); this.standardDeviation = getStandardDeviation(runtimeDependencies); this.calledBeans = getCalledBeans(runtimeDependencies); writeHeader(); final List<String> callerBeans = new ArrayList<String>(runtimeDependencies.keySet()); Collections.sort(callerBeans); final PdfPCell defaultCell = getDefaultCell(); boolean odd = false; for (final String callerBean : callerBeans) { if (odd) { defaultCell.setGrayFill(0.97f); } else { defaultCell.setGrayFill(1); } odd = !odd; // NOPMD final Map<String, Integer> beanDependencies = runtimeDependencies.get(callerBean); writeBeanDependencies(callerBean, beanDependencies); } addToDocument(currentTable); } private void writeBeanDependencies(String callerBean, Map<String, Integer> beanDependencies) { getDefaultCell().setHorizontalAlignment(Element.ALIGN_LEFT); addCell(callerBean, cellFont); getDefaultCell().setHorizontalAlignment(Element.ALIGN_RIGHT); for (final String calledBean : calledBeans) { final Integer dependency = beanDependencies.get(calledBean); if (dependency == null) { addCell("", cellFont); } else { final String s = dependency.toString(); if (dependency > standardDeviation * 2) { addCell(s, severeCellFont); } else if (dependency > standardDeviation) { addCell(s, warningCellFont); } else { addCell(s, cellFont); } } } } private void writeHeader() throws DocumentException { final List<String> headers = new ArrayList<String>(); headers.add("Beans"); headers.addAll(calledBeans); final int[] relativeWidths = new int[headers.size()]; Arrays.fill(relativeWidths, 0, headers.size(), 1); relativeWidths[0] = 4; final PdfPTable table = new PdfPTable(headers.size()); table.setWidthPercentage(100); table.setWidths(relativeWidths); table.setHeaderRows(1); final PdfPCell defaultCell = table.getDefaultCell(); defaultCell.setGrayFill(0.9f); defaultCell.setHorizontalAlignment(Element.ALIGN_CENTER); defaultCell.setVerticalAlignment(Element.ALIGN_MIDDLE); defaultCell.setPaddingLeft(0); defaultCell.setPaddingRight(0); for (final String header : headers) { table.addCell(new Phrase(header, boldCellFont)); // pas la première entête de colonne defaultCell.setRotation(90); } defaultCell.setRotation(0); defaultCell.setPaddingLeft(2); defaultCell.setPaddingRight(2); currentTable = table; } private PdfPCell getDefaultCell() { return currentTable.getDefaultCell(); } private void addCell(String string, Font font) { currentTable.addCell(new Phrase(string, font)); } }