P2JQueryStatistics.java

/*
** Module   : P2JQueryStatistics.java
** Abstract : Manages statistics for P2J queries
**
** Copyright (c) 2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------------Description---------------------------------------
** 001 RAA 20230522 First revision with basic usage.
** 002 RAA 20230802 Introduced two maps to store overall statistics.
**     RAA 20231122 Removed location and fql fields from main class. Refactored gatherQueryDetails to use
**                  indexed looping. Pre-computed hashCode for both subclasses.
*/

/*
** This program is free software: you can redistribute it and/or modify
** it under the terms of the GNU Affero General Public License as
** published by the Free Software Foundation, either version 3 of the
** License, or (at your option) any later version.
**
** This program is distributed in the hope that it will be useful,
** but WITHOUT ANY WARRANTY; without even the implied warranty of
** MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
** GNU Affero General Public License for more details.
**
** You may find a copy of the GNU Affero GPL version 3 at the following
** location: https://www.gnu.org/licenses/agpl-3.0.en.html
**
** Additional terms under GNU Affero GPL version 3 section 7:
**
**   Under Section 7 of the GNU Affero GPL version 3, the following additional
**   terms apply to the works covered under the License.  These additional terms
**   are non-permissive additional terms allowed under Section 7 of the GNU
**   Affero GPL version 3 and may not be removed by you.
**
**   0. Attribution Requirement.
**
**     You must preserve all legal notices or author attributions in the covered
**     work or Appropriate Legal Notices displayed by works containing the covered
**     work.  You may not remove from the covered work any author or developer
**     credit already included within the covered work.
**
**   1. No License To Use Trademarks.
**
**     This license does not grant any license or rights to use the trademarks
**     Golden Code, FWD, any Golden Code or FWD logo, or any other trademarks
**     of Golden Code Development Corporation. You are not authorized to use the
**     name Golden Code, FWD, or the names of any author or contributor, for
**     publicity purposes without written authorization.
**
**   2. No Misrepresentation of Affiliation.
**
**     You may not represent yourself as Golden Code Development Corporation or FWD.
**
**     You may not represent yourself for publicity purposes as associated with
**     Golden Code Development Corporation, FWD, or any author or contributor to
**     the covered work, without written authorization.
**
**   3. No Misrepresentation of Source or Origin.
**
**     You may not represent the covered work as solely your work.  All modified
**     versions of the covered work must be marked in a reasonable way to make it
**     clear that the modified work is not originating from Golden Code Development
**     Corporation or FWD.  All modified versions must contain the notices of
**     attribution required in this license.
*/

package com.goldencode.p2j.persist;

import java.util.*;

/**
 * Class responsible with storing information about P2J Queries.
 * For the moment, only {@link CompoundQuery} and {@link RandomAccessQuery} support this type
 * of storing statistics.
 */
public class P2JQueryStatistics
{
   /** Transient map of query instances to information about where they were constructed. */
   private static final Map<Key, P2JQueryStatistics> statistics = new LinkedHashMap<>();
   
   /** Map that stores the location of queries. */
   private static final Map<P2JQuery, Location> locations = new WeakHashMap<>();
   
   /** Number of times query was executed at this location (across query instances). */
   private long useCount = 0;
   
   /** The execution time of this query until this moment. */
   private double time = 0;
   
   /**
    * Default constructor.
    */
   private P2JQueryStatistics()
   {
      
   }
   
   /**
    * Function that retrieves/creates the statistics of a query.
    * 
    * @param   query
    *          The query in use.
    * @param   fql
    *          The FQL of the query.
    * 
    * @return  P2JQueryStatistics
    *          An instance of {@code P2JQueryStatistics} denoting the statistics of the given query.
    */
   public static P2JQueryStatistics getStatistics(P2JQuery query, String fql)
   {
      // First find/create the location of the query
      Location location = null;
      synchronized (locations)
      {
         location = locations.get(query);
      }
      
      if (location == null)
      {
         location = gatherQueryDetails();
         synchronized (locations)
         {
            locations.put(query, location);
         }
      }
      
      // Then access its statistics
      Key key = new Key(location, fql);
      P2JQueryStatistics info = null;
      synchronized (statistics)
      {
         info = statistics.get(key);
      }
      
      if (info == null)
      {
         info = new P2JQueryStatistics();
         synchronized (statistics)
         {
            statistics.put(key, info);
         }
      }
      
      return info;
   }
   
   /**
    * Function that returns the location of a query (if one exists), in a ready to be printed format.
    * 
    * @param   query
    *          The given query.
    *          
    * @return  String
    *          The formatted string that denotes the location of the query, or {@code null} if no such
    *          location exists.
    */
   public static String getLocationFormattedOutput(P2JQuery query)
   {
      Location location = null;
      synchronized (locations)
      {
         location = locations.get(query);
      }
      
      return location == null ? null : location.toString();
   }
   
   /**
    * Get the number of times the FQL query was executed at this location, across all query
    * instances constructed at the same location.
    * 
    * @return  long
    *          Query execution count.
    */
   public long getUseCount()
   {
      return useCount;
   }
   
   /**
    * Increment by one the number of times the FQL query was executed at this location, across
    * all query instances constructed at the same location.
    */
   public void incrementUseCount()
   {
      useCount++;
   }
   
   /**
    * Getter for the {@code time} field.
    * 
    * @return  double
    *          The execution time of the query until this moment.
    */
   public double getTime()
   {
      return time;
   }
   
   /**
    * Method for increasing the total time of this query by a specified amount.
    * 
    * @param   time
    *          The time that will be added to the total time.
    */
   public void increaseTime(double time)
   {
      this.time += time;
   }
   
   /**
    * Function that navigates the stack trace, identifies details of a query (such as file name, method name,
    * line number) and creates a {@code Location} based on those findings.
    * 
    * @return  Location
    *          The location of a query.
    */
   private static Location gatherQueryDetails()
   {
      Throwable t = new Throwable();
      String queryClass = null;
      String fileName = null;
      String methodName = null;
      String className = null;
      int lineNumber = -1;
      String previousFileName = null;
      String previousMethodName = null;
      String previousClassName = null;
      int previousLineNumber = -1;
      StackTraceElement[] stackTraceElements = t.getStackTrace();
      for (int i = 0; i < stackTraceElements.length; i++)
      {
         className = stackTraceElements[i].getClassName();
         fileName = stackTraceElements[i].getFileName();
         lineNumber = stackTraceElements[i].getLineNumber();
         methodName = stackTraceElements[i].getMethodName();
         
         if (queryClass != null && !fileName.equals(queryClass))
         {
            return new Location(previousFileName, previousLineNumber, previousClassName, previousMethodName);
         }
         else if (queryClass == null && className.lastIndexOf('$') < 0 && !fileName.startsWith("P2JQuery"))
         {
            queryClass = fileName;
         }
         
         previousClassName = className;
         previousFileName = fileName;
         previousLineNumber = lineNumber;
         previousMethodName = methodName;
      }
      
      return null;
   }
   
   /**
    * Helper class which stores location information regarding the construction of a P2J query.
    */
   private static class Location
   {
      /** Unqualified application source code file name containing query. */
      private final String sourceFile;
      
      /** Line number at which query was constructed. */
      private final int lineNumber;
      
      /** Name of application class (sans package root name) containing query. */
      private final String className;
      
      /** Name of application-level method in which query was executed. */
      private final String methodName;
      
      /** The hash code of this {@code Location}. */
      private final int hashCode;
      
      /**
       * Constructor.
       * 
       * @param   sourceFile
       *          Unqualified application source code file name containing query.
       * @param   lineNumber
       *          Line number at which query was constructed.
       * @param   className
       *          Name of application class (sans package root name) containing query.
       * @param   methodName
       *          Name of application-level method in which query was executed
       */
      private Location(String sourceFile, int lineNumber, String className, String methodName)
      {
         this.sourceFile = sourceFile.intern();
         this.lineNumber = lineNumber;
         this.className = className.intern();
         this.methodName = methodName.intern();
         
         hashCode =  37 * 
                    (37 * 
                    (37 * 
                    (37 * 17 + lineNumber) 
                             + className.hashCode()) 
                             + methodName.hashCode()) 
                             + sourceFile.hashCode();
      }
      
      /**
       * Return the hash code computed for this object instance.
       * 
       * @return  int
       *          Hash code.
       */
      @Override
      public int hashCode()
      {
         return hashCode;
      }
      
      /**
       * Indicate whether this object instance is equivalent to the given instance, based on
       * the content of both.
       * 
       * @return  boolean
       *          {@code true} if equivalent, {@code false} otherwise.
       */
      @Override
      public boolean equals(Object o)
      {
         if (!(o instanceof Location))
         {
            return false;
         }
         
         Location that = (Location) o;
         
         return this.lineNumber == that.lineNumber &&
                this.className  == that.className  &&
                this.methodName == that.methodName &&
                this.sourceFile == that.sourceFile;
      }
      
      /**
       * A formatted way of presenting a {@code Location}.
       * 
       * @return  String
       *          The {@code String} representing the {@code Location}.
       */
      @Override
      public String toString()
      {
         StringBuilder result = new StringBuilder();
         result.append("Source file: ").append(sourceFile)
               .append("; Method name: ").append(methodName)
               .append("; Line number: ").append(lineNumber);
         return result.toString();
      }
   }
   
   /**
    * A hashable key comprised of both location information and an FQL string. Location info
    * alone is not enough to differentiate queries, because different FQL can be executed
    * from a query constructed at the same code location.
    */
   private static class Key
   {
      /** Query construction location information. */
      private final Location location;
      
      /** Dialect-specific FQL query string. */
      private final String fql;
      
      /** The hash code of this {@code Key}. */
      private final int hashCode;
      
      /**
       * Constructor.
       * 
       * @param   location
       *          Query construction location information.
       * @param   fql
       *          Dialect-specific FQL query string.
       */
      Key(Location location, String fql)
      {
         this.location = location;
         this.fql = fql;
         
         hashCode = 37 * (37 * 17 + location.hashCode()) + fql.hashCode();
      }
      
      /**
       * Return the hash code computed for this object instance.
       * 
       * @return  int
       *          Hash code.
       */
      public int hashCode()
      {
         return hashCode;
      }
      
      /**
       * Indicate whether this object instance is equivalent to the given instance, based on
       * the content of both.
       * 
       * @return  boolean
       *          {@code true} if equivalent, {@code false} otherwise.
       */
      public boolean equals(Object o)
      {
         if (!(o instanceof Key))
         {
            return false;
         }
         
         Key that = (Key) o;
         return (this.fql.equals(that.fql) && this.location.equals(that.location));
      }
   }
}