SimpleQueryProfiler.java

/*
** Module   : SimpleQueryProfiler.java
** Abstract : Simple query profiler implementing the query profiler MBean
**
** Copyright (c) 2022, Golden Code Development Corporation.
**
** -#- -I- --Date-- ---------------------------------Description----------------------------------
** 001 AL2  20220828 Created initial version.
*/

/*
** 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.jmx;

import java.io.BufferedWriter;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;

/** Simple query profiler implementing the query profiler MBean */
public class SimpleQueryProfiler
extends Facultative
implements SimpleQueryProfilerMBean,
           QueryProfiler
{
   /** Accumulated values */
   private Map<String, Map<String, AtomicLong>> queries = new ConcurrentHashMap<>();
   
   /** The types of counters for each query*/
   enum QueryInfo 
   {
      /** Represent the number of rows extracted from a result set */
      ROWS_COUNT,
      /** Represent the number of cache hits done by each query */
      CACHE_HITS,
      /** Represent the number of cache misses done by each query */
      CACHE_MISSES,
      /** Represent the number nano-seconds spent to hydrate */
      HYDRATE_TOTAL_TIME
   }
   
   /**
    * Update the number of rows retrieved from a result set
    *
    * @param query
    *        the query for which the update is done
    * @param value
    *        new value
    */
   public void updateRowsCount(String query, long value)
   {
      queries.computeIfAbsent(query, k -> new ConcurrentHashMap<>())
             .computeIfAbsent(QueryInfo.ROWS_COUNT.toString(), k -> new AtomicLong(0))
             .addAndGet(value);
   }
   
   /**
    * Update the number of cache hits of a specified query
    *
    * @param query
    *        the query for which the update is done
    * @param value
    *        the number of cache hits
    */
   public void updateCacheHits(String query, long value)
   {
      queries.computeIfAbsent(query, k -> new ConcurrentHashMap<>())
             .computeIfAbsent(QueryInfo.CACHE_HITS.toString(), k -> new AtomicLong(0))
             .addAndGet(value);
   }
   
   /**
    * Update the number of cache misses of a specified query
    *
    * @param query
    *        the query for which the update is done
    * @param value
    *        the number of cache misses
    */
   public void updateCacheMisses(String query, long value)
   {
      queries.computeIfAbsent(query, k -> new ConcurrentHashMap<>())
             .computeIfAbsent(QueryInfo.CACHE_MISSES.toString(), k -> new AtomicLong(0))
             .addAndGet(value);
   }

   /**
    * Update the number of nanoseconds spent in hydrate of a specified query
    *
    * @param query
    *        the query for which the update is done
    * @param value
    *        the number of nanoseconds spent in hydrate
    */
   @Override
   public void updateHydrateTotalTime(String query, long value)
   {
      queries.computeIfAbsent(query, k -> new ConcurrentHashMap<>())
             .computeIfAbsent(QueryInfo.HYDRATE_TOTAL_TIME.toString(), k -> new AtomicLong(0))
             .addAndGet(value);
   }

   /**
    * Retrieve the values accumulated for each query.
    */
   @Override
   public Map<String, Map<String, Long>> getValues()
   {
      Map<String, Map<String, Long>> sortedMap = new TreeMap<>();
      for (Map.Entry<String, Map<String, AtomicLong>> entry : queries.entrySet())
      {
         Map<String, Long> specs = new TreeMap<>();
         sortedMap.put(entry.getKey(), specs);
         for (Map.Entry<String, AtomicLong> spec : entry.getValue().entrySet())
         {
            specs.put(spec.getKey(), spec.getValue().get());
         }
      }
      return sortedMap;
   }


   /**
    * Retrieve the number of queries profiled
    */
   @Override
   public long getQueryCount()
   {
      return queries.size();
   }
   
   /** 
    * Reset profiling 
    */
   public void reset()
   {
      queries.clear();
   }
   
   /**
    * Print the profiling numbers done for each query
    * 
    * @param   file 
    *          output file name (will be appended if exists)
    * @throws  IOException 
    *          on I/O ERROR
    */
   @Override
   public void print(String file) 
   throws IOException
   {
      if (file == null || file.trim().isEmpty())
      {
         file = "fwd-counters.txt";
      }
      String ts = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
      try(FileWriter fw = new FileWriter(file, true);
          BufferedWriter bw = new BufferedWriter(fw);
          PrintWriter out = new PrintWriter(bw))
      {
         for (Map.Entry<String, Map<String, AtomicLong>> entry : queries.entrySet())
         {
            StringBuilder sb = new StringBuilder();
            for (Map.Entry<String, AtomicLong> spec : entry.getValue().entrySet())
            {
               sb.append(spec.getKey() + "=" + spec.getValue().get());
               sb.append(", ");
            }
            out.println(toString(ts, entry.getKey() + ": " + sb.toString()));
         }
      } 
   }

   /**
    * Simple implementation of toString. The timestamp is not relevant.
    */
   @Override
   public String toString(String ts, String comment) 
   {
      return comment;
   }
}