package edu.stanford.nlp.sempre.cache; import fig.basic.*; import java.io.BufferedReader; import java.io.File; import java.io.IOException; import java.io.PrintWriter; import java.nio.file.FileSystems; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.StandardCopyOption; import java.util.LinkedHashMap; import java.util.Map; /** * Cache backed by a file. * * @author Percy Liang */ public class FileStringCache implements StringCache, LruCallback<String, String> { public static class Options { @Option(gloss = "Cache capacity (in MB)") public int capacity = 35 * 1024; @Option(gloss = "Auto-flush cache to disk every N accesses") public int flushFrequency = Integer.MAX_VALUE; @Option(gloss = "Append mode instead of dump mode") public boolean appendMode = true; public int verbose = 0; } public static final Options opts = new Options(); private String path; private PrintWriter out; private final LinkedHashMap<String, String> cache; private final StatFig keyStats = new StatFig(); private final StatFig valStats = new StatFig(); private int numTouches = 0; private int numEvictions = 0; private boolean readOnly; public FileStringCache() { int cap = opts.capacity; cap = (cap < 0) ? cap : (cap * 1024 * 1024); if (cap < 0) { cache = new LinkedHashMap<String, String>(); } else { cache = new LruMap<String, String>(cap, this); } } public String getPath() { return path; } public void init(String path) { init(path, false); } public void init(String path, boolean readOnly) { if (this.path != null) throw new RuntimeException("Already initialized with " + this.path); this.path = path; this.readOnly = readOnly; // Read existing. if (new File(path).exists()) { try { BufferedReader in = IOUtils.openInHard(path); String line; while ((line = in.readLine()) != null) { String[] tokens = line.split("\t", 2); if (tokens.length != 2) throw new RuntimeException("Invalid line in cache file: " + line); cache.put(tokens[0], tokens[1]); } } catch (IOException e) { throw new RuntimeException(e); } } LogInfo.logs("Using cache %s (%d entries)", path, cache.size()); if (!readOnly && opts.appendMode) out = IOUtils.openOutAppendHard(path); flush(); } private void flush() { if (readOnly) return; if (out != null) // Append mode return; if (path == null) // No file-backing return; if (opts.verbose >= 2) { LogInfo.begin_track("FileStringCache FLUSH (dump mode)"); LogInfo.logs("Size: %d", size()); if (cache instanceof LruMap) LogInfo.logs("Memory: %d", ((LruMap<String, String>) cache).getBytes()); LogInfo.logs("Touches: %d", numTouches); LogInfo.logs("Evictions: %d", numEvictions); LogInfo.logs("Evicted keys: %s", keyStats); LogInfo.logs("Evicted values: %s", valStats); LogInfo.end_track(); } PrintWriter dumpOut = IOUtils.openOutHard(this.path + ".tmp"); for (Map.Entry<String, String> entry : cache.entrySet()) { dumpOut.println(entry.getKey() + "\t" + entry.getValue()); } dumpOut.flush(); dumpOut.close(); try { Path src = FileSystems.getDefault().getPath(this.path + ".tmp"); Path dst = FileSystems.getDefault().getPath(this.path); Files.move(src, dst, StandardCopyOption.REPLACE_EXISTING, StandardCopyOption.ATOMIC_MOVE); } catch (IOException e) { throw new RuntimeException(e); } } public String get(String key) { return cache.get(key); } public void put(String key, String value) { assert key.indexOf('\t') == -1 : key; assert key.indexOf('\n') == -1 : key; assert value.indexOf('\n') == -1 : value; if (opts.verbose >= 5) { logTrack("FileStringCache PUT (before)", key, value); } cache.put(key, value); if (out != null) { // Append mode out.println(key + "\t" + value); out.flush(); } if (numTouches++ % opts.flushFrequency == 0) flush(); } public int size() { return cache.size(); } @Override public void onEvict(Map.Entry<String, String> entry) { if (opts.verbose >= 5) { logTrack("FileStringCache EVICT (after)", entry.getKey(), entry.getValue()); } numEvictions++; keyStats.add(entry.getKey(), entry.getKey().length()); valStats.add(entry.getValue(), entry.getValue().length()); } private void logTrack(String header, String key, String value) { LogInfo.begin_track(header); LogInfo.logs("Key size: %d (%d bytes)", key.length(), MemUsage.getBytes(key)); LogInfo.logs("Val size: %d (%d bytes)", value.length(), MemUsage.getBytes(value)); if (cache instanceof LruMap) { LogInfo.logs("Cache size: %d entries (%d bytes of %d)", cache.size(), ((LruMap) cache).getBytes(), ((LruMap) cache).getCapacity()); } LogInfo.end_track(); } // For tests @Deprecated public int getNumTouches() { return numTouches; } }