/* This file is part of VoltDB. * Copyright (C) 2008-2017 VoltDB Inc. * * 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 should have received a copy of the GNU Affero General Public License * along with VoltDB. If not, see <http://www.gnu.org/licenses/>. */ package org.voltdb.compiler; import java.io.Serializable; import java.util.ArrayList; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; import java.util.Timer; import java.util.TimerTask; import org.voltdb.common.Constants; import org.voltdb.planner.BoundPlan; import org.voltdb.utils.Encoder; import com.google_voltpatches.common.cache.Cache; import com.google_voltpatches.common.cache.CacheBuilder; /** * Keep a cache two level cache of plans generated by the Ad Hoc * planner. * * First, store literals mapping to full plans that are * ready to execute. * * Second, store a string representation of a parameterized parsed * statement mapped to core parameterized plans. These parameterized * plans need parameter values and sql literals in order to be * actually used. */ public class AdHocCompilerCache implements Serializable { private static final long serialVersionUID = 1L; ////////////////////////////////////////////////////////////////////////// // STATIC CODE TO MANAGE CACHE LIFETIMES / GLOBALNESS ////////////////////////////////////////////////////////////////////////// // weak values should remove the object when the catalog hash is no longer needed private static Cache<String, AdHocCompilerCache> m_catalogHashMatch = CacheBuilder.newBuilder().weakValues().build(); public static void clearHashCache() { m_catalogHashMatch.invalidateAll(); } /** * Get the global cache for a given hash of the catalog. Note that there can be only * one cache per catalogHash at a time. */ public synchronized static AdHocCompilerCache getCacheForCatalogHash(byte[] catalogHash) { String hashString = Encoder.hexEncode(catalogHash); AdHocCompilerCache cache = m_catalogHashMatch.getIfPresent(hashString); if (cache == null) { cache = new AdHocCompilerCache(); m_catalogHashMatch.put(hashString, cache); } return cache; } ////////////////////////////////////////////////////////////////////////// // PER-INSTANCE AWESOMEC CACHING CODE ////////////////////////////////////////////////////////////////////////// // cache sizes determined at construction time final int MAX_LITERAL_ENTRIES; // max cache size for parameterized plans final long MAX_LITERAL_MEM = Long.getLong("ADHOC_COMPILER_CACHE_MAX_LITERAL_MEM_BYTES", 32*1024*1024); final int MAX_CORE_ENTRIES; /** cache of literals to full plans */ final Map<String, AdHocPlannedStatement> m_literalCache; /** cache of parameterized plan descriptions to one or more core parameterized plans, * each plan optionally has its own requirements for which parameters need to be bound * to what values to enable its specialized (expression-indexed) plan. */ final Map<String, List<BoundPlan> > m_coreCache; // placeholder stats used during development that may/may not survive long m_literalHits = 0; long m_literalQueries = 0; long m_literalInsertions = 0; long m_literalEvictions = 0; long m_planHits = 0; long m_planQueries = 0; long m_planInsertions = 0; long m_planEvictions = 0; /** {@see this#startPeriodicStatsPrinting() } */ Timer m_statsTimer = null; /** * Constructor with default cache sizes. */ private AdHocCompilerCache() { this(1000, 1000); } /** * Constructor with specific cache sizes is only called directly for testing. * * @param maxLiteralEntries cache size for literals * @param maxLiteralMem cache memory for literals */ AdHocCompilerCache(int maxLiteralEntries, int maxCoreEntries) { MAX_LITERAL_ENTRIES = maxLiteralEntries; MAX_CORE_ENTRIES = maxCoreEntries; // an LRU cache map m_literalCache = new AdHocStatementCache(MAX_LITERAL_ENTRIES, MAX_LITERAL_MEM); // an LRU cache map m_coreCache = new LinkedHashMap<String, List<BoundPlan> >(MAX_CORE_ENTRIES * 2, .75f, true) { private static final long serialVersionUID = 1L; // This method is called just after a new entry has been added @Override public boolean removeEldestEntry(Map.Entry<String, List<BoundPlan> > eldest) { if (size() > MAX_CORE_ENTRIES) { ++m_planEvictions; return true; } return false; } }; } // define a LinkedHashMap based LRU cache bounds by both entry number and entry value on-heap size // without changing Map.Entry, only works for value of type AdHocPlannedStatement // only extend put, remove,clear and removeEldestEntry methods to account weight public class AdHocStatementCache extends LinkedHashMap<String, AdHocPlannedStatement>{ private static final long serialVersionUID = 2988383448026641836L; private final int maxEntries; private final long maxMemory; // in bytes private long currentMemory; // in bytes public AdHocStatementCache() { // default max entry of 1000 // default max value size of 32MB this(1000, 32 * 1024 * 1024); } public AdHocStatementCache(final int maxEntries) { this(maxEntries, 32 * 1024 * 1024); } public AdHocStatementCache(final int maxEntries, final long maxMemory) { // set accessOrder to true for LRU super(maxEntries * 2, .75f, true); this.maxEntries = maxEntries; this.maxMemory = maxMemory; this.currentMemory = 0; } // This method is called just after a new entry has been added @Override public boolean removeEldestEntry(final Map.Entry<String, AdHocPlannedStatement> eldest) { if ((size() > maxEntries) || (this.currentMemory > this.maxMemory)) { ++m_literalEvictions; this.currentMemory -= eldest.getValue().getSerializedSize(); return true; } return false; } @Override public AdHocPlannedStatement put(String key, AdHocPlannedStatement value) { this.currentMemory += value.getSerializedSize(); return super.put(key,value); } @Override public AdHocPlannedStatement remove(Object key) { AdHocPlannedStatement value = super.remove(key); if (value != null) { this.currentMemory -= value.getSerializedSize(); } return value; } @Override public void clear() { super.clear(); this.currentMemory = 0; } } /** * Stats printing method used during development. * Probably shouldn't live past real stats integration. */ synchronized void printStats() { String line1 = String.format("CACHE STATS - Literals: Hits %d/%d (%.1f%%), Inserts %d Evictions %d\n", m_literalHits, m_literalQueries, (m_literalHits * 100.0) / m_literalQueries, m_literalInsertions, m_literalEvictions); String line2 = String.format("CACHE STATS - Plans: Hits %d/%d (%.1f%%), Inserts %d Evictions %d\n", m_planHits, m_planQueries, (m_planHits * 100.0) /m_planQueries, m_planInsertions, m_planEvictions); System.out.print(line1 + line2); System.out.flush(); // reset these m_literalHits = 0; m_literalQueries = 0; m_literalInsertions = 0; m_literalEvictions = 0; m_planHits = 0; m_planQueries = 0; m_planInsertions = 0; m_planEvictions = 0; } /** * @param sql SQL literal * @return full, ready-to-go plan */ public synchronized AdHocPlannedStatement getWithSQL(String sql) { ++m_literalQueries; AdHocPlannedStatement retval = m_literalCache.get(sql); if (retval != null) { ++m_literalHits; } return retval; } /** * @param parsedToken String representing a parameterized and parsed * SQL statement * @return A CorePlan that needs parameter values to run. */ public synchronized List<BoundPlan> getWithParsedToken(String parsedToken) { ++m_planQueries; List<BoundPlan> retval = m_coreCache.get(parsedToken); if (retval != null) { ++m_planHits; } return retval; } /** * Called from the PlannerTool directly when it finishes planning. * This is the only way to populate the cache. * * Note that one goal here is to reduce the number of times two * separate plan instances with the same value are input for the * same SQL literal. * * L1 cache (literal cache) cache SQL queries without user provided parameters. * L2 cache (core cache) cache parameterized queries: including user parameters and auto extracted parameters. * * @param sql original query text * @param parsedToken massaged query text, possibly with literals purged * @param planIn * @param extractedLiterals the basis values for any "bound parameter" restrictions to plan re-use * @param hasUserQuestionMarkParameters is user provided parameterized query * @param hasAutoParameterizedException is the auto parameterized query has parameter exception */ public synchronized void put(String sql, String parsedToken, AdHocPlannedStatement planIn, String[] extractedLiterals, boolean hasUserQuestionMarkParameters, boolean hasAutoParameterizedException) { assert(sql != null); assert(parsedToken != null); assert(planIn != null); AdHocPlannedStatement plan = planIn; assert(new String(plan.sql, Constants.UTF8ENCODING).equals(sql)); // hasUserQuestionMarkParameters and hasAutoParameterizedException can not be true at the same time // it means that a query can not be both user parameterized query and auto parameterized query. assert(!hasUserQuestionMarkParameters || !hasAutoParameterizedException); // uncomment this to get some raw stdout cache performance stats every 5s //startPeriodicStatsPrinting(); // deal with L2 cache if (! hasAutoParameterizedException) { BoundPlan matched = null; BoundPlan unmatched = new BoundPlan(planIn.core, planIn.parameterBindings(extractedLiterals)); // deal with the parameterized plan cache first List<BoundPlan> boundVariants = m_coreCache.get(parsedToken); if (boundVariants == null) { boundVariants = new ArrayList<BoundPlan>(); m_coreCache.put(parsedToken, boundVariants); // Note that there is an edge case in which more than one plan is getting counted as one // "plan insertion". This only happens when two different plans arose from the same parameterized // query (token) because one invocation used the correct constants to trigger an expression index and // another invocation did not. These are not counted separately (which would have to happen below // after each call to boundVariants.add) because they are not evicted separately. // It seems saner to use consistent units when counting insertions vs. evictions. ++m_planInsertions; } else { for (BoundPlan boundPlan : boundVariants) { if (boundPlan.equals(unmatched)) { matched = boundPlan; break; } } if (matched != null) { // if a different core is found, reuse it // this is useful when updating the literal cache if (unmatched.m_core != matched.m_core) { plan = new AdHocPlannedStatement(planIn, matched.m_core); plan.setBoundConstants(matched.m_constants); } } } if (matched == null) { // Don't count insertions (of possibly repeated tokens) here // -- see the comment above where only UNIQUE token insertions are being counted, instead. boundVariants.add(unmatched); } } // then deal with the L1 cache if (! hasUserQuestionMarkParameters) { AdHocPlannedStatement cachedPlan = m_literalCache.get(sql); if (cachedPlan == null) { //* enable to debug */ System.out.println("DEBUG: Caching literal '" + sql + "'"); m_literalCache.put(sql, plan); ++m_literalInsertions; } else { assert(cachedPlan.equals(plan)); } } } /** * Start a timer that prints cache stats to the console every 5s. * Used for development until we get better stats integration. */ public void startPeriodicStatsPrinting() { if (m_statsTimer == null) { m_statsTimer = new Timer(); m_statsTimer.scheduleAtFixedRate(new TimerTask() { @Override public void run() { printStats(); } }, 5000, 5000); } } /** * Return the number of items in the literal cache. * @return literal cache size as a count */ public int getLiteralCacheSize() { return m_literalCache.size(); } /** * Return the number of items in the core (parameterized) cache. * @return core cache size as a count */ public int getCoreCacheSize() { return m_coreCache.size(); } }