AdaptiveQueryTraceAspect.java

/*
** Module   : AdaptiveQueryTraceAspect.java
** Abstract : Aspect to capture method trace data from AdaptiveQuery
**
** Copyright (c) 2022-2023, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 DDF 20221103 Created initial version.
** 002 DDF 20221114 Improved tracing of AdaptiveQuery methods.
*                   New methods can be easily added to tracing.
*                   When tracing is disabled, the data is written into
*                   a CSV file.
*  003 DDF 20221124 Added more methods for data collection and made small
*                   improvements.
** 004 DDF 20221128 Small changes to displayed data in the output file and
**                  fixed a problem where preselect and dynamic calls were
**                  wrongly assigned.
** 005 DDF 20221202 Display data about the parent methods from SingleData
**                  depending on the number of AdaptiveQuery instances that
**                  were created.
** 006 DDF 20221208 Fix wrong number of total invalidated queries displayed
**                  in overview.
** 007 RAA 20221221 Moved getStackMethodParent inside SQLStatementLogger class and redirected
**                  the calls for the method to that class.
** 008 GBB 20230512 Logging methods replaced by CentralLogger/ConversionStatus.
*/
/*
** 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.aspects.ltw;

import java.io.*;
import java.lang.reflect.Method;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.*;
import java.util.logging.*;

import com.goldencode.p2j.util.logging.*;
import org.aspectj.lang.*;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;

import com.goldencode.p2j.persist.*;
import com.goldencode.p2j.persist.event.RecordChangeEvent;
import com.goldencode.p2j.persist.lock.LockType;
import com.goldencode.p2j.persist.orm.*;

/**
 * An abstract aspect to capture profile data (invocation count and operation time) per AdaptiveQuery.
 * The aspect is designed for load-time weaving using concrete aspect subclass defined in an 
 * {@code aop.xml} file. That concrete aspect must define a pointcut for the abstract 
 * {@link #adaptiveScope()} method.
 */
@Aspect
public abstract class AdaptiveQueryTraceAspect
{
   /** Logger */
   private static final CentralLogger LOG = CentralLogger.get(AdaptiveQueryTraceAspect.class.getName());
   
   /** Flag which controls data collection */
   private static volatile boolean enabled = false;
   
   /** Map of profile data by method signature */
   private static final Map<AdaptiveQuery, SingleData> traceData = new ConcurrentHashMap<>();
   
   /** 
    * Map storing SingleData.parent and how many times
    * the parent triggered new AdaptiveQuery instances.
    */
   private static final Map<String, AtomicLong> parentCalls = new ConcurrentHashMap<>();
   
   /** Data storage for all AdaptiveQueries */
   private static OverviewData overviewData;
   
   /** List of methods that are traced */
   private static final List<Method> operations = new CopyOnWriteArrayList<>();
   
   /** Fixed name of CSV output file (written to current directory) */
   private static String CSV_FILE = "adaptive.csv";
   
   /** Flag that controls if all or just the invalidated queries will be displayed */
   private static final boolean OUTPUT_ALL_QUERIES = false;
   
   /** 
    * Value that controls the parents displayed after the overview data,
    * it will only display parents that were called more times than the
    * set value. 
    */
   private static final long PARENT_CALL_AMOUNT_LIMIT = 3L;
   
   /**
    * Method that initializes the operations traced in AdaptiveQuery
    * and creates an instance of OverviewData which depends on the 
    * number of operations traced.
    */
   static void initialize()
   {
      if (LOG.isLoggable(Level.WARNING))
      {
         LOG.warning("AdaptiveQuery profiling is enabled; " +
                  "this feature is not intended for production use!");
      }
      
      if (overviewData != null) 
      {
         return;
      }
      
      try
      {
         // Class that is traced
         Class<?> adaptiveClass = AdaptiveQuery.class;
         Method method = null;
         
         // TEMPLATE for tracing new methods
         //
         // If the method doesn't have parameters:
         // method = adaptiveClass.getDeclaredMethod("method_name")
         // If the method has one or more parameters:
         // method = adaptiveClass.getDeclaredMethod("method_name", TypeOne.class, ...)
         // 
         // If the method argument type is a class that can't be accessed directly:
         // Class.forName("package.className");
         // method = adaptiveClass.getDeclaredMethod("method_name", Class.forName("package.className"))
         //
         // If the method specified is protected or private, you need to make it accessible.
         // method.setAccessible(true);
         // Finally, add the operation to the list of operations.
         // operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("invalidate");
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("invalidate", AdaptiveComponent.class);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("invalidate", RecordChangeEvent.class);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("invalidateIfReferenceRowExists");
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("first", LockType.class);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("last", LockType.class);
         operations.add(method);

         method = adaptiveClass.getDeclaredMethod("next", LockType.class);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("previous", LockType.class);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("revalidate", Object.class);
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("resetDynamicScrolling");
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("reposition", int.class);
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("forward", int.class);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("repositionByID", Long[].class, boolean.class);
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("backward", int.class);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("executeQuery",
                                                  Persistence.class,
                                                  String.class,
                                                  Object[].class);
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("execute");
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("fetch",
                                                  boolean.class,
                                                  LockType.class,
                                                  boolean.class);
         method.setAccessible(true);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("stateChanged", RecordChangeEvent.class);
         operations.add(method);
         
         method = adaptiveClass.getDeclaredMethod("doubleCheckInvalidation", RecordChangeEvent.class);
         method.setAccessible(true);
         operations.add(method);
      }
      catch (NoSuchMethodException e)
      {
         LOG.warning("Method specified :" + e.getMessage() + " for tracing was not found.");
      }
      
      overviewData = new OverviewData();
   }
   
   /**
    * Enable or disable data collection. Upon disabling data collection, any collected results
    * are processed and exported.
    * 
    * @param   enable
    *          {@code true} to start data collection; {@code false} to stop it and process the
    *          results.
    */
   public synchronized static void enableDataCollection(boolean enable)
   {
      if (enable)
      {
         if (!enabled)
         {
            enabled = true;
            initialize();
         }
         
         return;
      }
      
      try
      {
         enabled = false;
         processData();
      }
      finally
      {
         traceData.clear();
      }
   }
   
   /**
    * Post-process the collected trace data and log the results.
    */
   public static void processData() 
   {
      Comparator<AdaptiveQuery> aqComparator = new Comparator<AdaptiveQuery>() 
      {
         @Override
         public int compare(AdaptiveQuery o1, AdaptiveQuery o2)
         {
            return Long.signum(traceData.get(o2).invalidationCount.get() - 
                               traceData.get(o1).invalidationCount.get());
         }
      };
      
      List<AdaptiveQuery> values = new ArrayList<>(traceData.keySet());
      Collections.sort(values, aqComparator);
      
      try (BufferedWriter bw = new BufferedWriter(new FileWriter(CSV_FILE)))
      {
         bw.append("\"Number of AdaptiveQueries:\",").append(Integer.toString(traceData.size()));
         bw.newLine();
         bw.newLine();
         int count = 1;
         int preselectQueryCount = 0;
         for (AdaptiveQuery adaptiveQuery : values)
         {
            SingleData sd = traceData.get(adaptiveQuery);
            
            if (sd.invalidationCount.get() == 0)
            {
               preselectQueryCount++;
               if (!OUTPUT_ALL_QUERIES)
               {
                  parentCalls.remove(sd.parent);
                  continue;
               }
            }
            
            bw.append("\"AdaptiveQuery:\",").append(Integer.toString(count++));
            bw.newLine();
            bw.append("\"Parent:\",").append(sd.parent);
            bw.newLine();
            bw.append("\"Tables joined:\",").append(Long.toString(sd.tableCount.get()));
            bw.newLine();
            bw.append("\"Scrolling:\",").append(Boolean.toString(sd.isScrolling));
            bw.newLine();
            bw.append("\"Invalidation count:\",").append(Long.toString(sd.invalidationCount.get()));
            bw.newLine();
            bw.append("\"Revalidation count:\",").append(Long.toString(sd.revalidationCount.get()));
            bw.newLine();
            bw.append("\"StateChanged count:\",").append(Long.toString(sd.stateChangedCount.get()));
            bw.newLine();
            bw.append("\"DoubleCheckInvalidation (true) count:\",")
              .append(Long.toString(sd.doubleCheckInvalidationCount.get(1)));
            bw.newLine();
            bw.append("\"DoubleCheckInvalidation (false) count:\",")
              .append(Long.toString(sd.doubleCheckInvalidationCount.get(0)));
            bw.newLine();
            if (sd.stateChangedCount.get() != 0)
            {
               bw.append("\"invalidate/stateChanged (ratio):\",")
                 .append(Double.toString(sd.invalidationCount.get() / 
                                         (double) sd.stateChangedCount.get()));
            }
            else 
            {
               bw.append("\"invalidate/stateChanged (ratio):\",").append("0");
            }
            bw.newLine();
            if (sd.doubleCheckInvalidationCount.get(0) != 0)
            {
               bw.append("\"doubleCheckInvalidation true/false (ratio):\",")
                 .append(Double.toString(sd.doubleCheckInvalidationCount.get(1) / 
                                         (double) sd.doubleCheckInvalidationCount.get(0)));
            }
            else 
            {
               bw.append("\"doubleCheckInvalidation true/false (ratio):\",").append("0");
            }
            bw.newLine();
            bw.append("\"Method\",\"Total number of calls\",\"Calls before the first invalidation\",")
              .append("\"Calls in Preselect Mode\",")
              .append("\"% of method calls in Preselect Mode from current Query\",")
              .append("\"Calls between the first invalidation-revalidation\",")
              .append("\"Calls in Dynamic Mode\",")
              .append("\"% of calls in Dynamic Mode from current Query\",")
              .append("\"Call time in Preselect Mode (nano)\",")
              .append("\"% of time in Preselect Mode from total time of the operation\",")
              .append("\"Call time in Dynamic Mode (nano)\",")
              .append("\"% of time in Dynamic Mode from total time of operation\",")
              .append("\"Total method time (nano)\",");
            bw.newLine();
            Long total = 0L;
            Long calls = sd.calls.get();
            for (int position = 0; position < operations.size(); position++)
            {
               Method method = operations.get(position);
               StringBuilder sb = new StringBuilder();
               sb.append(method.getName()).append("(");
               for (Class<?> param : method.getParameterTypes())
               {
                  sb.append(param.toString()).append(" ");
               }
               sb.append(")");
               String op = sb.toString().replace(";", "");
               bw.append(op).append(",");
               if (sd.operationsCallTime.get(position) != 0)
               {
                  bw.append(Long.toString(sd.operationsInPreselectMode.get(position) +
                                          sd.operationsInDynamicMode.get(position)))
                    .append(",")
                    .append(Long.toString(sd.operationsBeforeInvalidation.get(position))).append(",")
                    .append(Long.toString(sd.operationsInPreselectMode.get(position))).append(",");
                  if (overviewData.totalOperationsCallsInPreselectMode.get(position) != 0)
                  {
                     bw.append(Double.toString((sd.operationsInPreselectMode.get(position) / 
                               (double) (sd.operationsInPreselectMode.get(position) +
                                         sd.operationsInDynamicMode.get(position)))
                               * 100))
                       .append(",");
                     calls -= sd.operationsInPreselectMode.get(position);
                  }
                  else 
                  {
                     bw.append("0,");
                  }
                  bw.append(Long.toString(sd.operationsBetweenInvalidRevalid.get(position))).append(",")
                    .append(Long.toString(sd.operationsInDynamicMode.get(position))).append(",");
                  if (overviewData.totalOperationsCallsInDynamicMode.get(position) != 0)
                  {
                     bw.append(Double.toString((sd.operationsInDynamicMode.get(position) / 
                               (double) (sd.operationsInPreselectMode.get(position) +
                                         sd.operationsInDynamicMode.get(position)))
                               * 100))
                       .append(",");
                     calls -= sd.operationsInDynamicMode.get(position);
                  }
                  else 
                  {
                     bw.append("0,");
                  }
                  bw.append(Long.toString(sd.operationsTimeInPreselect.get(position))).append(",")
                    .append(calculatePercentage(sd.operationsTimeInPreselect,
                            sd.operationsCallTime,
                            position))
                    .append(",")
                    .append(Long.toString(sd.operationsTimeInDynamic.get(position))).append(",")
                    .append(calculatePercentage(sd.operationsTimeInDynamic,
                            sd.operationsCallTime,
                            position))
                    .append(",")
                    .append(Long.toString(sd.operationsCallTime.get(position)));
                  total += sd.operationsCallTime.get(position);
               }
               else 
               {
                  bw.append("0,0,0,0,0,0,0,0,0,0,0,0");
               }
               bw.newLine();
            }
            bw.append("\"Other\",")
               .append(Long.toString(calls))
               .append(",-,-,-,-,-,-,-,-,-,-,-");
            bw.newLine();
            bw.newLine();
         }
         
         double size = (double) traceData.size();
         
         bw.append("\"OVERVIEW Data\"");
         bw.newLine();
         bw.append("\"Total number of queries:\",")
           .append(Integer.toString(traceData.size()));
         bw.newLine();
         bw.append("\"Total number of invalidated queries:\",")
           .append(Integer.toString(traceData.size() - preselectQueryCount));
         bw.newLine();
         bw.append("\"Total invalidations:\",")
           .append(Long.toString(overviewData.totalInvalidations.get()));
         bw.newLine();
         bw.append("\"Average invalidation count:\",")
           .append(Double.toString(overviewData.totalInvalidations.get() / size));
         bw.newLine();
         bw.append("\"Total revalidations:\",")
           .append(Long.toString(overviewData.totalRevalidations.get()));
         bw.newLine();
         bw.append("\"Average revalidations count:\",")
           .append(Double.toString(overviewData.totalRevalidations.get() / size));
         bw.newLine();
         bw.append("\"Total adaptive calls:\",")
           .append(Long.toString(overviewData.totalAdaptiveCalls.get()));
         bw.newLine();
         bw.append("\"Total Scrolling count:\",")
           .append(Long.toString(overviewData.totalScrolling.get()));
         bw.newLine();
         bw.append("\"Total Non-scrolling count:\",")
           .append(Double.toString(size - overviewData.totalScrolling.get()));
         bw.newLine();
         bw.newLine();
         
         bw.append("\"Method\",\"Total method calls\",\"Average method calls\",")
           .append("\"Total method calls before first invalidation\",")
           .append("\"Average method calls before first invalidations\",")
           .append("\"Total method calls in preselect mode\",")
           .append("\"Average of method calls in preselect mode\",")
           .append("\"Total method calls between first invalidation and revalidation\",")
           .append("\"Average of method calls between first invalidation and revalidation\",")
           .append("\"Total method calls in dynamic mode\",")
           .append("\"Average method calls in dynamic mode\",")
           .append("\"Total method calls time in preselect mode (nano)\",")
           .append("\"Average of method calls time in preselect mode (nano)\",")
           .append("\"Total method calls time in dynamic mode (nano)\",")
           .append("\"Average of method calls time in dynamic mode (nano)\",")
           .append("\"Total method call time (nano)\",")
           .append("\"Average of method call time in any mode (nano)\"");
         bw.newLine();
         
         Long totalUntrackedCalls = overviewData.totalAdaptiveCalls.get();
         for (int position = 0; position < operations.size(); position++)
         {
            Method method = operations.get(position);
            StringBuilder sb = new StringBuilder();
            sb.append(method.getName()).append("( ");
            for (Class<?> param : method.getParameterTypes())
            {
               sb.append(param.getName().toString()).append(" ");
            }
            sb.append(")");
            String op = sb.toString().replace(";", "");
            bw.append(op).append(",");
            if (overviewData.totalOperationsCalls.get(position) != 0)
            {
               totalUntrackedCalls -= overviewData.totalOperationsCalls.get(position);
               bw.append(Long.toString(overviewData.totalOperationsCalls.get(position)))
                 .append(",")
                 .append(Double.toString(overviewData.totalOperationsCalls.get(position)
                                         / size))
                 .append(",")
                 .append(Long.toString(overviewData.totalOperationsCallsBeforeInvalidation.get(position)))
                 .append(",")
                 .append(Double.toString(overviewData.totalOperationsCallsBeforeInvalidation.get(position)
                                         / size))
                 .append(",")
                 .append(Long.toString(overviewData.totalOperationsCallsInPreselectMode.get(position)))
                 .append(",")
                 .append(Double.toString(overviewData.totalOperationsCallsInPreselectMode.get(position)
                                         / size))
                 .append(",")
                 .append(Long.toString(overviewData.totalOperationsBetweenInvalidRevalid.get(position)))
                 .append(",")
                 .append(Double.toString(overviewData.totalOperationsBetweenInvalidRevalid.get(position)
                                         / size))
                 .append(",")
                 .append(Long.toString(overviewData.totalOperationsCallsInDynamicMode.get(position)))
                 .append(",")
                 .append(Double.toString(overviewData.totalOperationsCallsInDynamicMode.get(position)
                                         / size))
                 .append(",")
                 .append(Long.toString(overviewData.totalOperationsCallsTimeInPreselect.get(position)))
                 .append(",")
                 .append(Double.toString(overviewData.totalOperationsCallsTimeInPreselect.get(position)
                                         / size))
                 .append(",")
                 .append(Long.toString(overviewData.totalOperationsCallsTimeInDynamic.get(position)))
                 .append(",")
                 .append(Double.toString(overviewData.totalOperationsCallsTimeInDynamic.get(position)
                                         / size))
                 .append(",")
                 .append(Long.toString(overviewData.totalOperationsCallsTime.get(position)))
                 .append(",")
                 .append(Double.toString(overviewData.totalOperationsCallsTime.get(position)
                                         / size));
            }
            else 
            {
               bw.append("0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0");
            }
            bw.newLine();
         }
         bw.append("\"Other\",")
           .append(Long.toString(totalUntrackedCalls))
           .append(",-,-,-,-,-,-,-,-,-,-,-,-,-,-,-");
         
         Comparator<String> parentCallsComparator = new Comparator<String>() 
         {
            @Override
            public int compare(String o1, String o2)
            {
               return Long.signum(parentCalls.get(o2).get() -
                                  parentCalls.get(o1).get());
            }
         };
         
         List<String> parentCallsValues = new ArrayList<>(parentCalls.keySet());
         Collections.sort(parentCallsValues, parentCallsComparator);
         
         bw.newLine();
         bw.newLine();
         bw.append("\"PARENT calls\"");
         bw.newLine();
         for (String parentCall : parentCallsValues)
         {
            long value = parentCalls.get(parentCall).get();
            if (value >= PARENT_CALL_AMOUNT_LIMIT)
            {
               bw.append(parentCall)
                 .append(",")
                 .append(Long.toString(value));
               bw.newLine();
            }
         }
      }
      catch (IOException e) 
      {
         LOG.log(Level.SEVERE, "I/O error writing trace data CSV", e);
      }
   }
   
   /**
    * Calculate the percentage of a value from a total in
    * an AtomicLongArray, being given a position.
    * 
    * @param   value
    *          Value divided by the total
    * @param   total
    *          Total for division
    * @param   position
    *          Position of the value and total
    *          in the AtomicLongArray
    *          
    * @return  A double representing the percentage
    */
   public static String calculatePercentage(AtomicLongArray value, AtomicLongArray total, int position)
   {
      return Double.toString((value.get(position) / (double) total.get(position)) * 100);
   }
   
   /**
    * An abstract pointcut which should be defined for load-time weaving inside a {@code
    * concrete-aspect} element within {@code META-INF/aop.xml}.
    */
   @Pointcut
   public abstract void adaptiveScope();
   
   /**
    * The primary tracing advice which collects trace information about calls to the
    * AdaptiveQuery class. Each AdaptiveQuery instance will have a SingleData instance
    * associated with it, while also collecting data in the OverviewData from all the
    * existent queries.
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    *          
    * @return  Object, if any, returned by the method being traced.
    * 
    * @throws  Throwable
    *          if an exception is thrown by the method being traced.
    */
   @Around("adaptiveScope()")
   public Object trace(ProceedingJoinPoint joinPoint)
   throws Throwable
   {
      Object target = joinPoint.getTarget();
      if (checkNotEnable(joinPoint) || target == null || !(target instanceof AdaptiveQuery))
      {
         return joinPoint.proceed();
      }
      
      AdaptiveQuery aq = (AdaptiveQuery) target;
      SingleData data = traceData.get(aq);
      
      if (data == null)
      {
         data = new SingleData();
         handleAdaptiveQueryDetails(joinPoint, data);
         handleAdaptiveQueryScrollingStatus(joinPoint, data);
         data.tableCount.set((long) aq.getTableCount());
         SingleData existing = traceData.putIfAbsent(aq, data);
         if (existing != null)
         {
            data = existing;
         }
      }
      
      handleOperation(joinPoint, data);
      handleInvalidation(joinPoint, data);
      handleRevalidation(joinPoint, data);
      handleStateChanged(joinPoint, data);
      
      Object rt = null;
      int operationCall = getOperationIfAny(joinPoint);
      boolean isDoubleCheck = isDoubleCheckMethod(joinPoint);
      boolean updateTableCount = checkUpdateTableCount(joinPoint);
      long start = System.nanoTime();
      try
      {
         rt = joinPoint.proceed();
      }
      finally 
      {
         long elapsed = System.nanoTime() - start;
         if (enabled)
         {
            overviewData.totalAdaptiveCalls.incrementAndGet();
            overviewData.totalAdaptiveQueryTime.addAndGet(elapsed);
            
            if (operationCall != -1) 
            {
               if (aq.isPreselect())
               {
                  overviewData.totalOperationsCallsInPreselectMode.incrementAndGet(operationCall);
                  data.operationsInPreselectMode.incrementAndGet(operationCall);
                  overviewData.totalOperationsCallsTimeInPreselect.addAndGet(operationCall, elapsed);
                  data.operationsTimeInPreselect.addAndGet(operationCall, elapsed);
               }
               else
               {
                  overviewData.totalOperationsCallsInDynamicMode.incrementAndGet(operationCall);
                  data.operationsInDynamicMode.incrementAndGet(operationCall);
                  overviewData.totalOperationsCallsTimeInDynamic.addAndGet(operationCall, elapsed);
                  data.operationsTimeInDynamic.addAndGet(operationCall, elapsed);
               }
               overviewData.totalOperationsCallsTime.addAndGet(operationCall, elapsed);
               data.operationsCallTime.addAndGet(operationCall, elapsed);
            }
            
            if (isDoubleCheck)
            {
               boolean returnValue = (boolean) rt;
               if (returnValue)
               {
                  overviewData.totalDoubleCheckInvalidation.incrementAndGet(1);
                  data.doubleCheckInvalidationCount.incrementAndGet(1);
               }
               else 
               {
                  overviewData.totalDoubleCheckInvalidation.incrementAndGet(0);
                  data.doubleCheckInvalidationCount.incrementAndGet(0);
               }
            }
            
            if (updateTableCount)
            {
               data.tableCount.set((long) aq.getTableCount());
            }
         }
      }
      
      return rt;
   }
   
   /**
    * Displays data about the current traced AdaptiveQuery method.
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    * @param   data
    *          Information of an AdaptiveQuery.
    */
   public void handleAdaptiveQueryDetails(ProceedingJoinPoint joinPoint, SingleData data)
   {
      StackTraceElement[] ste = Thread.currentThread().getStackTrace();
      String stackParent = getStackMethodParent(ste);
      
      if (stackParent != null)
      {
         AtomicLong parentValue = parentCalls.get(stackParent);
         AtomicLong newValue;
         if (parentValue != null)
         {
            newValue = new AtomicLong(parentValue.incrementAndGet());
         }
         else 
         {
            newValue = new AtomicLong(1);
         }
         parentCalls.put(stackParent, newValue);
         data.parent = stackParent;
      }
   }
   
   /**
    * Introduces new data to an existent SingleData instance belonging to
    * an AdaptiveQuery regarding it's Scrolling status.
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    * @param   data
    *          Information of an AdaptiveQuery.
    */
   public void handleAdaptiveQueryScrollingStatus(ProceedingJoinPoint joinPoint, SingleData data)
   {
      AdaptiveQuery aq = (AdaptiveQuery) joinPoint.getTarget();
      if (aq != null && aq.isScrolling())
      {
         data.isScrolling = true;
         overviewData.totalScrolling.incrementAndGet();
      }
   }
   
   /**
    * Introduces new data to an existent SingleData instance belonging to
    * an AdaptiveQuery when the operation is AdaptiveQuery.invalidate().
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    * @param   data
    *          Information of an AdaptiveQuery.
    */
   public void handleInvalidation(ProceedingJoinPoint joinPoint, SingleData data)
   {
      Signature signature = joinPoint.getSignature();
      Object[] args = joinPoint.getArgs();
      if (signature.getName().equals("invalidate") &&
          args.length == 1 &&
          args[0] instanceof AdaptiveComponent)
      {
         overviewData.totalInvalidations.incrementAndGet();
         data.invalidationCount.incrementAndGet();
      }
   }
   
   /**
    * Introduces new data to an existent SingleData instance belonging to
    * an AdaptiveQuery when the operation is AdaptiveQuery.revalidate().
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    * @param   data
    *          Information of an AdaptiveQuery.
    */
   public void handleRevalidation(ProceedingJoinPoint joinPoint, SingleData data)
   {
      Signature signature = joinPoint.getSignature();
      if (signature.getName().equals("revalidate") && joinPoint.getArgs().length == 1)
      {
         overviewData.totalRevalidations.incrementAndGet();
         data.revalidationCount.incrementAndGet();
      }
   }
   
   /**
    * Introduces new data to an existent SingleData instance belonging to
    * an AdaptiveQuery when the operation is AdaptiveQuery.stateChanged().
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    * @param   data
    *          Information of an AdaptiveQuery.
    */
   public void handleStateChanged(ProceedingJoinPoint joinPoint, SingleData data)
   {
      Signature signature = joinPoint.getSignature();
      if (signature.getName().equals("stateChanged") && joinPoint.getArgs().length == 1)
      {
         overviewData.totalStateChanged.incrementAndGet();
         data.stateChangedCount.incrementAndGet();
      }
   }
   
   /**
    * Introduces new data to an existent SingleData instance belonging to
    * an AdaptiveQuery when the operation was defined in the specific
    * operation list.
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    * @param   data
    *          Information of an AdaptiveQuery.
    */
   public void handleOperation(ProceedingJoinPoint joinPoint, SingleData data)
   {
      int operation = getOperationIfAny(joinPoint);
      if (operation != -1)
      {
         overviewData.totalOperationsCalls.incrementAndGet(operation);
         if (data.invalidationCount.get() == 0)
         {
            overviewData.totalOperationsCallsBeforeInvalidation.incrementAndGet(operation);
            data.operationsBeforeInvalidation.incrementAndGet(operation);
         }
         
         if (data.invalidationCount.get() == 1 && data.revalidationCount.get() == 0)
         {
            overviewData.totalOperationsBetweenInvalidRevalid.incrementAndGet(operation);
            data.operationsBetweenInvalidRevalid.incrementAndGet(operation);
         }
      }
      data.calls.incrementAndGet();
   }
   
   /**
    * Check if the current method is AdaptiveQuery.execute().
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    *          
    * @return  true/false
    */
   public boolean checkUpdateTableCount(ProceedingJoinPoint joinPoint)
   {
      // Initially tableCount is 0 and then components are added
      // The value of SingleData.tableCount should be updated when
      // the "execute" method is called.
      Signature signature = joinPoint.getSignature();
      if (signature.getName().equals("execute"))
      {
         return true;
      }
      return false;
   }
   
   /**
    * Checks if the current method is AdaptiveQuery.doubleCheckInvalidation().
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    *
    * @return  true/false
    */
   public boolean isDoubleCheckMethod(ProceedingJoinPoint joinPoint)
   {
      Signature signature = joinPoint.getSignature();
      if (signature.getName().equals("doubleCheckInvalidation"))
      {
         return true;
      }
      return false;
   }
   
   /**
    * Check if the current traced method is an operation that
    * exists in the list of traced methods.
    * 
    * @param   joinPoint
    *          Join point which represents the method being traced.
    *          
    * @return  -1 if it's not an operation
    *          <int> value corresponding to the position in the list of methods
    */
   public int getOperationIfAny(ProceedingJoinPoint joinPoint)
   {
      MethodSignature ms = (MethodSignature) joinPoint.getSignature();
      
      for (int i = 0; i < operations.size(); i++)
      {
         if (ms.getMethod().equals(operations.get(i)))
         {
            return i;
         }
      }
      
      return -1;
   }
   
   /**
    * Check if tracing is enabled or method traced is one of the methods
    * that can cause recursive calls. 
    * 
    * If a particular information is required from an AdaptiveQuery 
    * and it can be obtained from one of the methods present
    * in the class, make sure that it is ignored when tracing.
    * 
    * @param   pjp
    *          Join point which represents the method being traced.
    *          
    * @return  true if method should be ignored
    *          false if method should proceed and be traced
    */
   public boolean checkNotEnable(ProceedingJoinPoint pjp)
   {
      String name = pjp.getSignature().getName();
      if (!enabled || 
          name.equals("isScrolling") || 
          name.equals("isNativelyScrolling") ||
          name.equals("isPreselect") ||
          name.equals("getTableCount"))
      {
         return true;
      }
      return false;
   }
   
   /**
    * Search for the parent that eventually led to the AdaptiveQuery in a StackTrace.
    * The parent is associated with the method from the converted 4GL application.
    * 
    * @param   ste
    *          A StackTraceElement array containing the current thread trace
    *          until it reached the trace method.
    *          
    * @return  null if there is no parent
    *          a String value that holds the first parent found in the stack trace
    */
   public String getStackMethodParent(StackTraceElement[] ste)
   {
      return SQLStatementLogger.getStackMethodParent(ste);
   }
   
   /**
    * Nested class used to keep track of information belonging to all of the
    * AdaptiveQueries traced. It is useful for calculating average and percentage
    * values. 
    */
   private static class OverviewData
   {
      /** Total number of invalidations */
      final AtomicLong totalInvalidations = new AtomicLong(0);
      
      /** Total number of revalidations */
      final AtomicLong totalRevalidations = new AtomicLong(0);
      
      /** Total number of stateChangedCalls */
      final AtomicLong totalStateChanged = new AtomicLong(0);
      
      /** Total number of false and true (array order) values returned by the doubleCheckInvalidation */
      final AtomicLongArray totalDoubleCheckInvalidation = new AtomicLongArray(2);
      
      /** Total number of calls to any method */
      final AtomicLong totalAdaptiveCalls = new AtomicLong(0);
      
      /** Total number of scrolling queries */
      final AtomicLong totalScrolling = new AtomicLong(0);
      
      /** Total number of calls to traced methods */
      final AtomicLongArray totalOperationsCalls = new AtomicLongArray(operations.size());
      
      /** Total number of calls to traced methods before the first invalidation */
      final AtomicLongArray totalOperationsCallsBeforeInvalidation = new AtomicLongArray(operations.size());
      
      /** Total number of calls to traced methods between the first invalidation-revalidation action */
      final AtomicLongArray totalOperationsBetweenInvalidRevalid = new AtomicLongArray(operations.size());
      
      /** Total number of calls to traced methods while the Query is in Preselect mode */
      final AtomicLongArray totalOperationsCallsInPreselectMode = new AtomicLongArray(operations.size());
      
      /** Total number of calls to traced methods while the Query is in Dynamic mode */
      final AtomicLongArray totalOperationsCallsInDynamicMode = new AtomicLongArray(operations.size());
      
      /** Total time spent by the Query */
      final AtomicLong totalAdaptiveQueryTime = new AtomicLong(0);
      
      /** Total time spent in the traced methods while the Query are in Preselect mode */
      final AtomicLongArray totalOperationsCallsTimeInPreselect = new AtomicLongArray(operations.size());
      
      /** Total time spent in the traced methods while the Query are in Dynamic mode */
      final AtomicLongArray totalOperationsCallsTimeInDynamic = new AtomicLongArray(operations.size());
      
      /** Total time spent in the traced methods. */
      final AtomicLongArray totalOperationsCallsTime = new AtomicLongArray(operations.size());
   }
   
   /**
    * Nested class used to keep track of information belonging to only one
    * AdaptiveQuery that is traced.
    */
   private static class SingleData
   {
      /** Value for the scrolling status of the Query, default false */
      boolean isScrolling = false;
      
      /** Parent that generated the AdaptiveQuery creation */
      String parent;
      
      /** Number of tables joined by the Query */
      final AtomicLong tableCount = new AtomicLong(0);
      
      /** Number of invalidation of the current Query */
      final AtomicLong invalidationCount = new AtomicLong(0);

      /** Number of revalidations of the current Query */
      final AtomicLong revalidationCount = new AtomicLong(0);
      
      /** Number of stateChanged calls of the current Query */
      final AtomicLong stateChangedCount = new AtomicLong(0);
      
      /** Number of false and true (array order) values returned by the doubleCheckInvalidation */
      final AtomicLongArray doubleCheckInvalidationCount = new AtomicLongArray(2);
      
      /** Total number of calls to any method */
      final AtomicLong calls = new AtomicLong(0);
      
      /** Number of "operation" calls before the first invalidation */
      final AtomicLongArray operationsBeforeInvalidation = new AtomicLongArray(operations.size());
      
      /** Number of "operation" calls while the Query is in Preselect mode */
      final AtomicLongArray operationsInPreselectMode = new AtomicLongArray(operations.size());
      
      /** Number of "operation" calls while the query is in Dynamic mode */
      final AtomicLongArray operationsInDynamicMode = new AtomicLongArray(operations.size());
      
      /** Number of "operation" calls between the first invalidation-revalidation action */
      final AtomicLongArray operationsBetweenInvalidRevalid = new AtomicLongArray(operations.size());

      /** Time spent in the "operation" while the Query is in Preselect mode */
      final AtomicLongArray operationsTimeInPreselect = new AtomicLongArray(operations.size());
      
      /** Time spent in the "operation" method while the Query is in Dynamic mode */
      final AtomicLongArray operationsTimeInDynamic = new AtomicLongArray(operations.size());
      
      /** Time spent in the "operation" */
      final AtomicLongArray operationsCallTime = new AtomicLongArray(operations.size());
   }
}