snes-disassembler
Version:
A Super Nintendo (SNES) ROM disassembler for 65816 assembly
367 lines • 13.4 kB
JavaScript
"use strict";
/**
* ROM Analysis Cache System
*
* Reduces redundant ROM analysis calls by implementing intelligent caching
* and streamlined validation steps while maintaining analysis integrity.
*/
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
Object.defineProperty(exports, "__esModule", { value: true });
exports.globalROMCache = exports.ROMAnalysisCache = void 0;
const logger_1 = require("./utils/logger");
const crypto = __importStar(require("crypto"));
/**
* Centralized cache for ROM analysis results to eliminate redundant calculations
*/
class ROMAnalysisCache {
constructor(config = {}) {
this.memoryCache = new Map();
this.stats = { hits: 0, misses: 0, evictions: 0, cacheSize: 0 };
// Recursion guard: Track keys currently being processed
this.currentlyProcessing = new Set();
this.recursionDepth = new Map();
this.MAX_RECURSION_DEPTH = 5;
this.config = {
maxCacheAge: 1000 * 60 * 30, // 30 minutes
maxCacheSize: 100,
enableMemoryCache: true,
enablePersistentCache: false,
cacheValidation: true,
...config
};
this.logger = (0, logger_1.createLogger)('ROMAnalysisCache');
}
/**
* Generate cache key based on ROM content and analysis parameters
*/
generateCacheKey(rom, analysisType, params) {
const romHash = this.calculateROMHash(rom.data);
const paramsHash = params ? crypto.createHash('md5').update(JSON.stringify(params)).digest('hex').substring(0, 8) : '';
return `${analysisType}_${romHash}_${paramsHash}`;
}
/**
* Calculate fast hash of ROM data for cache key generation
*/
calculateROMHash(romData) {
// Use a subset of ROM data for faster hashing while maintaining uniqueness
const sampleSize = Math.min(0x8000, romData.length); // 32KB sample
const sample = Buffer.concat([
romData.slice(0, Math.min(0x200, romData.length)), // Header area
romData.slice(Math.max(0, romData.length - 0x200)), // Vector area
romData.slice(0x8000, 0x8000 + Math.min(0x1000, romData.length - 0x8000)) // Code area sample
]);
return crypto.createHash('md5').update(sample).digest('hex').substring(0, 16);
}
/**
* Check if cache entry is valid and not expired
*/
isValidCacheEntry(entry) {
const now = Date.now();
const isExpired = (now - entry.timestamp) > this.config.maxCacheAge;
return !isExpired;
}
/**
* Check if a key is currently being processed (recursion guard)
*/
isCurrentlyProcessing(key) {
return this.currentlyProcessing.has(key);
}
/**
* Mark a key as currently being processed and track recursion depth
*/
markAsProcessing(key) {
const currentDepth = this.recursionDepth.get(key) || 0;
if (currentDepth >= this.MAX_RECURSION_DEPTH) {
this.logger.warn(`Maximum recursion depth (${this.MAX_RECURSION_DEPTH}) exceeded for cache key`, {
key: key.substring(0, 16) + '...',
depth: currentDepth
});
return false;
}
if (this.currentlyProcessing.has(key)) {
this.logger.warn(`Recursion detected for cache key`, {
key: key.substring(0, 16) + '...',
depth: currentDepth
});
return false;
}
this.currentlyProcessing.add(key);
this.recursionDepth.set(key, currentDepth + 1);
this.logger.debug(`Marking key as processing`, {
key: key.substring(0, 16) + '...',
depth: currentDepth + 1
});
return true;
}
/**
* Unmark a key as currently being processed
*/
unmarkAsProcessing(key) {
this.currentlyProcessing.delete(key);
const currentDepth = this.recursionDepth.get(key) || 0;
if (currentDepth <= 1) {
this.recursionDepth.delete(key);
}
else {
this.recursionDepth.set(key, currentDepth - 1);
}
this.logger.debug(`Unmarking key as processing`, {
key: key.substring(0, 16) + '...',
depth: Math.max(0, currentDepth - 1)
});
}
/**
* Get cached analysis result if available and valid
*/
get(rom, analysisType, params) {
if (!this.config.enableMemoryCache)
return null;
const key = this.generateCacheKey(rom, analysisType, params);
// Check for recursion
if (this.isCurrentlyProcessing(key)) {
this.logger.warn(`Recursion attempt detected for cache get operation`, {
analysisType,
key: key.substring(0, 16) + '...',
currentDepth: this.recursionDepth.get(key) || 0
});
return null;
}
const entry = this.memoryCache.get(key);
if (entry && this.isValidCacheEntry(entry)) {
this.stats.hits++;
this.logger.debug(`Cache HIT for ${analysisType}`, { key: key.substring(0, 16) + '...' });
return entry.data;
}
this.stats.misses++;
this.logger.debug(`Cache MISS for ${analysisType}`, { key: key.substring(0, 16) + '...' });
return null;
}
/**
* Store analysis result in cache
*/
set(rom, analysisType, data, params) {
if (!this.config.enableMemoryCache)
return;
const key = this.generateCacheKey(rom, analysisType, params);
// Check for recursion
if (this.isCurrentlyProcessing(key)) {
this.logger.warn(`Recursion attempt detected for cache set operation`, {
analysisType,
key: key.substring(0, 16) + '...',
currentDepth: this.recursionDepth.get(key) || 0
});
return;
}
// Implement LRU eviction if cache is full
if (this.memoryCache.size >= this.config.maxCacheSize) {
this.evictOldestEntry();
}
const entry = {
hash: key,
timestamp: Date.now(),
data,
metadata: {
romSize: rom.data.length,
cartridgeType: rom.cartridgeInfo.type,
version: '1.0'
}
};
this.memoryCache.set(key, entry);
this.stats.cacheSize = this.memoryCache.size;
this.logger.debug(`Cached ${analysisType}`, {
key: key.substring(0, 16) + '...',
size: this.memoryCache.size
});
}
/**
* Evict oldest cache entry (LRU)
*/
evictOldestEntry() {
let oldestKey = '';
let oldestTimestamp = Date.now();
for (const [key, entry] of this.memoryCache.entries()) {
if (entry.timestamp < oldestTimestamp) {
oldestTimestamp = entry.timestamp;
oldestKey = key;
}
}
if (oldestKey) {
this.memoryCache.delete(oldestKey);
this.stats.evictions++;
this.logger.debug(`Evicted cache entry`, { key: oldestKey.substring(0, 16) + '...' });
}
}
/**
* Invalidate all cache entries for a specific ROM
*/
invalidateROM(rom) {
const romHash = this.calculateROMHash(rom.data);
const keysToDelete = [];
for (const key of this.memoryCache.keys()) {
if (key.includes(romHash)) {
keysToDelete.push(key);
}
}
keysToDelete.forEach(key => this.memoryCache.delete(key));
this.stats.cacheSize = this.memoryCache.size;
this.logger.info(`Invalidated ${keysToDelete.length} cache entries for ROM`);
}
/**
* Clear all cache entries
*/
clear() {
const size = this.memoryCache.size;
this.memoryCache.clear();
this.stats.cacheSize = 0;
this.logger.info(`Cleared ${size} cache entries`);
}
/**
* Get cache statistics
*/
getStats() {
const total = this.stats.hits + this.stats.misses;
const hitRate = total > 0 ? (this.stats.hits / total) * 100 : 0;
return {
...this.stats,
hitRate: Math.round(hitRate * 100) / 100
};
}
/**
* Begin processing for a specific analysis operation (public API)
* Returns false if recursion is detected or max depth exceeded
*/
beginProcessing(rom, analysisType, params) {
const key = this.generateCacheKey(rom, analysisType, params);
return this.markAsProcessing(key);
}
/**
* End processing for a specific analysis operation (public API)
*/
endProcessing(rom, analysisType, params) {
const key = this.generateCacheKey(rom, analysisType, params);
this.unmarkAsProcessing(key);
}
/**
* Check if a specific analysis operation is currently being processed
*/
isProcessing(rom, analysisType, params) {
const key = this.generateCacheKey(rom, analysisType, params);
return this.isCurrentlyProcessing(key);
}
/**
* Get current recursion depth for a specific operation
*/
getRecursionDepth(rom, analysisType, params) {
const key = this.generateCacheKey(rom, analysisType, params);
return this.recursionDepth.get(key) || 0;
}
/**
* Clear all processing state (useful for cleanup after errors)
*/
clearProcessingState() {
const processingCount = this.currentlyProcessing.size;
const depthCount = this.recursionDepth.size;
this.currentlyProcessing.clear();
this.recursionDepth.clear();
if (processingCount > 0 || depthCount > 0) {
this.logger.warn(`Cleared orphaned processing state`, {
processingKeys: processingCount,
depthEntries: depthCount
});
}
}
/**
* Convenience methods for specific analysis types
*/
getROMInfo(rom) {
return this.get(rom, ROMAnalysisCache.CACHE_KEYS.ROM_INFO);
}
setROMInfo(rom) {
this.set(rom, ROMAnalysisCache.CACHE_KEYS.ROM_INFO, rom);
}
getVectors(rom) {
return this.get(rom, ROMAnalysisCache.CACHE_KEYS.VECTORS);
}
setVectors(rom, vectors) {
this.set(rom, ROMAnalysisCache.CACHE_KEYS.VECTORS, vectors);
}
getBankLayout(rom) {
return this.get(rom, ROMAnalysisCache.CACHE_KEYS.BANK_LAYOUT);
}
setBankLayout(rom, bankLayout) {
this.set(rom, ROMAnalysisCache.CACHE_KEYS.BANK_LAYOUT, bankLayout);
}
getFunctions(rom, params) {
return this.get(rom, ROMAnalysisCache.CACHE_KEYS.FUNCTIONS, params);
}
setFunctions(rom, functions, params) {
this.set(rom, ROMAnalysisCache.CACHE_KEYS.FUNCTIONS, functions, params);
}
getValidationResult(rom, params) {
return this.get(rom, ROMAnalysisCache.CACHE_KEYS.VALIDATION, params);
}
setValidationResult(rom, result, params) {
this.set(rom, ROMAnalysisCache.CACHE_KEYS.VALIDATION, result, params);
}
getAudioState(rom) {
return this.get(rom, ROMAnalysisCache.CACHE_KEYS.AUDIO_STATE);
}
setAudioState(rom, audioState) {
this.set(rom, ROMAnalysisCache.CACHE_KEYS.AUDIO_STATE, audioState);
}
getDisassembly(rom, params) {
return this.get(rom, ROMAnalysisCache.CACHE_KEYS.DISASSEMBLY, params);
}
setDisassembly(rom, lines, params) {
this.set(rom, ROMAnalysisCache.CACHE_KEYS.DISASSEMBLY, lines, params);
}
}
exports.ROMAnalysisCache = ROMAnalysisCache;
// Cache keys for different analysis types
ROMAnalysisCache.CACHE_KEYS = {
ROM_INFO: 'rom_info',
VECTORS: 'vectors',
BANK_LAYOUT: 'bank_layout',
FUNCTIONS: 'functions',
DATA_STRUCTURES: 'data_structures',
VALIDATION: 'validation',
AUDIO_STATE: 'audio_state',
DISASSEMBLY: 'disassembly'
};
/**
* Singleton cache instance for global use
*/
exports.globalROMCache = new ROMAnalysisCache();
//# sourceMappingURL=analysis-cache.js.map