Press n or j to go to the next uncovered block, b, p or k for the previous block.
| 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 | 104x 104x 104x 104x 104x 104x 104x 104x 104x 1x 611x 611x 611x 1x 611x 611x 110x 109x 611x 611x 611x 624x 624x 31x 31x 31x 31x 593x 593x 593x 21x 4x 4x 3x 3x 3x 3x 2x 2x 2x 2x 2x 3x 1x 112x 157x 109x 3x 109x 109x 109x 6192x 111x 111x 109x 109x 109x 109x 109x 109x 109x 109x 6x 6x 6x 6x 6x 32x 276x 32x 32x 2x 3x 3x 3x 3x 3x 3x 2x 2x 2x 2x 2x 2x 2x 1x 2x 1x 1x 1x 1x 2x 1x | /**
* AudioCache - LRU cache for audio assets
* 오디오 캐시 - 오디오 자산을 위한 LRU 캐시
*
* Manages audio asset memory with automatic eviction of least-recently-used assets
* 최근 사용 빈도가 낮은 자산을 자동으로 제거하여 오디오 자산 메모리를 관리합니다
*/
import type { AudioAsset } from "./types";
/**
* Configuration for AudioCache
* AudioCache 구성
*/
export interface AudioCacheConfig {
/** Maximum cache size in bytes (default: 30MB) */
readonly maxSizeBytes: number;
/** Asset IDs that should never be evicted */
readonly criticalAssets: readonly string[];
/** Enable debug logging */
readonly debug?: boolean;
}
/**
* Cache entry with metadata
* 메타데이터가 있는 캐시 항목
*/
interface CacheEntry {
readonly asset: AudioAsset;
readonly size: number;
lastAccessed: number;
isCritical: boolean; // Mutable to allow dynamic updates via updateCriticalAssets()
}
/**
* Cache statistics
* 캐시 통계
*/
export interface CacheStats {
readonly totalSize: number;
readonly assetCount: number;
readonly criticalCount: number;
readonly utilizationPercent: number;
readonly evictionCount: number;
readonly hitCount: number;
readonly missCount: number;
readonly hitRate: number;
}
/**
* AudioCache - LRU cache implementation for audio assets
* 오디오 자산을 위한 LRU 캐시 구현
*/
export class AudioCache {
private cache: Map<string, CacheEntry> = new Map();
private currentSize: number = 0;
private maxSize: number;
private criticalAssets: Set<string>;
private debug: boolean;
private evictionCount: number = 0;
private hitCount: number = 0;
private missCount: number = 0;
constructor(config: AudioCacheConfig) {
this.maxSize = config.maxSizeBytes;
this.criticalAssets = new Set(config.criticalAssets);
this.debug = config.debug ?? false;
if (this.debug) {
console.log(
`[AudioCache] Initialized with max size: ${(this.maxSize / 1024 / 1024).toFixed(1)}MB, critical assets: ${this.criticalAssets.size}`
);
}
}
/**
* Add asset to cache with LRU tracking
* LRU 추적으로 캐시에 자산 추가
*
* @param id - Asset ID
* @param asset - Audio asset
* @param sizeBytes - Estimated size in bytes
*/
set(id: string, asset: AudioAsset, sizeBytes: number): void {
const isCritical = this.criticalAssets.has(id);
// If asset already exists, remove its size first
const existing = this.cache.get(id);
if (existing) {
this.currentSize -= existing.size;
}
// Check if we need to evict (only for new assets or size increases)
const needsSpace = this.currentSize + sizeBytes > this.maxSize;
if (needsSpace) {
while (
this.currentSize + sizeBytes > this.maxSize &&
this.canEvict()
) {
this.evictLRU();
}
}
// Add to cache
this.cache.set(id, {
asset,
size: sizeBytes,
lastAccessed: Date.now(),
isCritical,
});
this.currentSize += sizeBytes;
Iif (this.debug) {
console.log(
`[AudioCache] Added: ${id} (${(sizeBytes / 1024).toFixed(1)}KB)${isCritical ? " [CRITICAL]" : ""} - Total: ${(this.currentSize / 1024 / 1024).toFixed(1)}MB / ${(this.maxSize / 1024 / 1024).toFixed(1)}MB (${((this.currentSize / this.maxSize) * 100).toFixed(1)}%)`
);
}
}
/**
* Get asset from cache and update access time
* 캐시에서 자산을 가져오고 액세스 시간 업데이트
*
* @param id - Asset ID
* @returns Audio asset or undefined if not found
*/
get(id: string): AudioAsset | undefined {
const cached = this.cache.get(id);
if (cached) {
cached.lastAccessed = Date.now(); // Update LRU
this.hitCount++;
Iif (this.debug) {
console.log(`[AudioCache] Hit: ${id}`);
}
return cached.asset;
}
this.missCount++;
Iif (this.debug) {
console.log(`[AudioCache] Miss: ${id}`);
}
return undefined;
}
/**
* Check if asset exists in cache
* 캐시에 자산이 있는지 확인
*
* @param id - Asset ID
* @returns True if asset is cached
*/
has(id: string): boolean {
return this.cache.has(id);
}
/**
* Remove asset from cache
* 캐시에서 자산 제거
*
* @param id - Asset ID
* @returns True if asset was removed
*/
remove(id: string): boolean {
const entry = this.cache.get(id);
if (entry) {
this.cache.delete(id);
this.currentSize -= entry.size;
Iif (this.debug) {
console.log(
`[AudioCache] Removed: ${id} (${(entry.size / 1024).toFixed(1)}KB)`
);
}
// Unload audio element to free memory
if (entry.asset && "src" in entry.asset) {
const audioElement = entry.asset as AudioAsset & {
src?: string;
pause?: () => void;
};
Eif (audioElement.pause) {
audioElement.pause();
}
Eif (audioElement.src !== undefined) {
audioElement.src = "";
}
}
return true;
}
return false;
}
/**
* Check if can evict (has non-critical assets)
* 제거 가능 여부 확인 (중요하지 않은 자산이 있는지)
*
* @returns True if there are non-critical assets to evict
*/
private canEvict(): boolean {
for (const entry of this.cache.values()) {
if (!entry.isCritical) {
return true;
}
}
return false;
}
/**
* Evict least-recently-used non-critical asset
* 최근 사용 빈도가 가장 낮은 중요하지 않은 자산 제거
*/
private evictLRU(): void {
let oldestId: string | null = null;
let oldestTime = Infinity;
// Find oldest non-critical asset
for (const [id, entry] of this.cache) {
if (!entry.isCritical && entry.lastAccessed < oldestTime) {
oldestId = id;
oldestTime = entry.lastAccessed;
}
}
Eif (oldestId) {
const evicted = this.cache.get(oldestId);
Iif (!evicted) return; // Should not happen, but handle safely
this.cache.delete(oldestId);
this.currentSize -= evicted.size;
this.evictionCount++;
Iif (this.debug) {
const age = Date.now() - evicted.lastAccessed;
console.log(
`[AudioCache] Evicted: ${oldestId} (${(evicted.size / 1024).toFixed(1)}KB, age: ${(age / 1000).toFixed(1)}s) - Total: ${(this.currentSize / 1024 / 1024).toFixed(1)}MB`
);
}
// Unload audio element to free memory
if (evicted.asset && "src" in evicted.asset) {
const audioElement = evicted.asset as AudioAsset & {
src?: string;
pause?: () => void;
};
Eif (audioElement.pause) {
audioElement.pause();
}
Eif (audioElement.src !== undefined) {
audioElement.src = "";
}
}
}
}
/**
* Get cache statistics
* 캐시 통계 가져오기
*
* @returns Cache statistics
*/
getStats(): CacheStats {
const criticalCount = Array.from(this.cache.values()).filter(
(e) => e.isCritical
).length;
const hitRate =
this.hitCount + this.missCount > 0
? this.hitCount / (this.hitCount + this.missCount)
: 0;
return {
totalSize: this.currentSize,
assetCount: this.cache.size,
criticalCount,
utilizationPercent: (this.currentSize / this.maxSize) * 100,
evictionCount: this.evictionCount,
hitCount: this.hitCount,
missCount: this.missCount,
hitRate,
};
}
/**
* Clear entire cache
* 전체 캐시 지우기
*/
clear(): void {
// Unload all audio elements
for (const entry of this.cache.values()) {
Eif (entry.asset && "src" in entry.asset) {
const audioElement = entry.asset as AudioAsset & {
src?: string;
pause?: () => void;
};
Eif (audioElement.pause) {
audioElement.pause();
}
Eif (audioElement.src !== undefined) {
audioElement.src = "";
}
}
}
this.cache.clear();
this.currentSize = 0;
// Reset statistics counters to maintain accurate statistics after clear
this.evictionCount = 0;
this.hitCount = 0;
this.missCount = 0;
Iif (this.debug) {
console.log("[AudioCache] Cleared entire cache and reset statistics");
}
}
/**
* Get all cached asset IDs
* 모든 캐시된 자산 ID 가져오기
*
* @returns Array of asset IDs
*/
getCachedAssetIds(): readonly string[] {
return Array.from(this.cache.keys());
}
/**
* Get detailed cache information for debugging
* 디버깅을 위한 상세 캐시 정보 가져오기
*/
getDebugInfo(): {
readonly entries: ReadonlyArray<{
id: string;
size: number;
isCritical: boolean;
lastAccessed: number;
age: number;
}>;
readonly stats: CacheStats;
} {
const now = Date.now();
const entries = Array.from(this.cache.entries()).map(([id, entry]) => ({
id,
size: entry.size,
isCritical: entry.isCritical,
lastAccessed: entry.lastAccessed,
age: now - entry.lastAccessed,
}));
// Sort by last accessed (oldest first)
entries.sort((a, b) => a.lastAccessed - b.lastAccessed);
return {
entries,
stats: this.getStats(),
};
}
/**
* Update critical assets list
* 중요 자산 목록 업데이트
*
* @param criticalAssets - New list of critical asset IDs
*/
updateCriticalAssets(criticalAssets: readonly string[]): void {
this.criticalAssets = new Set(criticalAssets);
// Update isCritical flag for existing entries
for (const [id, entry] of this.cache) {
entry.isCritical = this.criticalAssets.has(id);
}
Iif (this.debug) {
console.log(
`[AudioCache] Updated critical assets: ${this.criticalAssets.size}`
);
}
}
}
|