Skip to content

Commit da87f3a

Browse files
committed
chore: wip
1 parent ef3e5b1 commit da87f3a

File tree

6 files changed

+501
-18
lines changed

6 files changed

+501
-18
lines changed

packages/launchpad/bin/cli.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1539,6 +1539,33 @@ cli
15391539
}
15401540
})
15411541

1542+
cli
1543+
.command('benchmark:cache', 'Benchmark cache lookup performance (in-memory vs disk)')
1544+
.option('--iterations <number>', 'Number of iterations per test (default: 10000)')
1545+
.option('--verbose', 'Show detailed output')
1546+
.option('--json', 'Output results as JSON')
1547+
.example('launchpad benchmark:cache')
1548+
.example('launchpad benchmark:cache --iterations 50000')
1549+
.example('launchpad benchmark:cache --json')
1550+
.action(async (options?: {
1551+
iterations?: string
1552+
verbose?: boolean
1553+
json?: boolean
1554+
}) => {
1555+
try {
1556+
const cmd = await resolveCommand('benchmark:cache')
1557+
if (!cmd)
1558+
return
1559+
const code = await cmd.run({ argv: [], options, env: process.env })
1560+
if (typeof code === 'number' && code !== 0)
1561+
process.exit(code)
1562+
}
1563+
catch (error) {
1564+
console.error('Benchmark failed:', error instanceof Error ? error.message : String(error))
1565+
process.exit(1)
1566+
}
1567+
})
1568+
15421569
// NOTE: cli.parse() moved to the end of the file to ensure all commands are registered
15431570

15441571
// Database management commands

packages/launchpad/src/cache.ts

Lines changed: 221 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,227 @@ const CACHE_DIR = path.join(homedir(), '.cache', 'launchpad')
1010
const BINARY_CACHE_DIR = path.join(CACHE_DIR, 'binaries', 'packages')
1111
const CACHE_METADATA_FILE = path.join(CACHE_DIR, 'cache-metadata.json')
1212

13+
// Shell environment cache configuration
14+
const SHELL_CACHE_DIR = path.join(CACHE_DIR, 'shell_cache')
15+
const ENV_CACHE_FILE = path.join(SHELL_CACHE_DIR, 'env_cache')
16+
17+
// In-memory cache for fast lookups (singleton pattern)
18+
interface EnvCacheEntry {
19+
projectDir: string
20+
depFile: string
21+
depMtime: number
22+
envDir: string
23+
}
24+
25+
class EnvCacheManager {
26+
private cache: Map<string, EnvCacheEntry> = new Map()
27+
private loaded: boolean = false
28+
29+
/**
30+
* Load the entire cache file into memory for O(1) lookups
31+
*/
32+
load(): void {
33+
if (this.loaded)
34+
return
35+
36+
try {
37+
if (fs.existsSync(ENV_CACHE_FILE)) {
38+
const content = fs.readFileSync(ENV_CACHE_FILE, 'utf-8')
39+
const lines = content.trim().split('\n')
40+
41+
for (const line of lines) {
42+
if (!line)
43+
continue
44+
45+
const [projectDir, depFile, depMtime, envDir] = line.split('|')
46+
if (projectDir && envDir) {
47+
this.cache.set(projectDir, {
48+
projectDir,
49+
depFile: depFile || '',
50+
depMtime: Number.parseInt(depMtime || '0', 10),
51+
envDir,
52+
})
53+
}
54+
}
55+
}
56+
57+
this.loaded = true
58+
}
59+
catch (error) {
60+
if (config.verbose) {
61+
console.warn('Failed to load environment cache:', error)
62+
}
63+
}
64+
}
65+
66+
/**
67+
* Get cached environment for a project directory
68+
*/
69+
get(projectDir: string): EnvCacheEntry | null {
70+
if (!this.loaded)
71+
this.load()
72+
73+
return this.cache.get(projectDir) || null
74+
}
75+
76+
/**
77+
* Set cached environment for a project directory
78+
*/
79+
set(projectDir: string, depFile: string, envDir: string): void {
80+
if (!this.loaded)
81+
this.load()
82+
83+
// Get mtime of dependency file if it exists
84+
let depMtime = 0
85+
if (depFile && fs.existsSync(depFile)) {
86+
const stats = fs.statSync(depFile)
87+
depMtime = Math.floor(stats.mtimeMs / 1000)
88+
}
89+
90+
const entry: EnvCacheEntry = {
91+
projectDir,
92+
depFile,
93+
depMtime,
94+
envDir,
95+
}
96+
97+
// Update in-memory cache immediately (instant for next lookups)
98+
this.cache.set(projectDir, entry)
99+
100+
// Schedule async disk write (don't block)
101+
this.schedulePersist()
102+
}
103+
104+
private persistTimer: NodeJS.Timeout | null = null
105+
private persistPending: boolean = false
106+
107+
/**
108+
* Schedule a persist operation (debounced to batch multiple writes)
109+
*/
110+
private schedulePersist(): void {
111+
if (this.persistTimer) {
112+
// Already scheduled, just mark as pending
113+
this.persistPending = true
114+
return
115+
}
116+
117+
// Schedule persist after a short delay to batch multiple writes
118+
this.persistTimer = setTimeout(() => {
119+
this.persistTimer = null
120+
this.persist()
121+
}, 10) // 10ms debounce
122+
}
123+
124+
/**
125+
* Persist cache to disk
126+
*/
127+
private persist(): void {
128+
try {
129+
fs.mkdirSync(SHELL_CACHE_DIR, { recursive: true })
130+
131+
const lines: string[] = []
132+
for (const entry of this.cache.values()) {
133+
lines.push(`${entry.projectDir}|${entry.depFile}|${entry.depMtime}|${entry.envDir}`)
134+
}
135+
136+
// Write atomically using temp file
137+
const tempFile = `${ENV_CACHE_FILE}.tmp.${process.pid}`
138+
fs.writeFileSync(tempFile, lines.join('\n') + '\n')
139+
fs.renameSync(tempFile, ENV_CACHE_FILE)
140+
}
141+
catch (error) {
142+
if (config.verbose) {
143+
console.warn('Failed to persist environment cache:', error)
144+
}
145+
}
146+
}
147+
148+
/**
149+
* Clear the cache
150+
*/
151+
clear(): void {
152+
this.cache.clear()
153+
this.loaded = false
154+
155+
try {
156+
if (fs.existsSync(ENV_CACHE_FILE)) {
157+
fs.unlinkSync(ENV_CACHE_FILE)
158+
}
159+
}
160+
catch (error) {
161+
if (config.verbose) {
162+
console.warn('Failed to clear environment cache:', error)
163+
}
164+
}
165+
}
166+
167+
/**
168+
* Get cache statistics
169+
*/
170+
getStats(): { entries: number, size: number } {
171+
if (!this.loaded)
172+
this.load()
173+
174+
let size = 0
175+
try {
176+
if (fs.existsSync(ENV_CACHE_FILE)) {
177+
const stats = fs.statSync(ENV_CACHE_FILE)
178+
size = stats.size
179+
}
180+
}
181+
catch {
182+
// Ignore errors
183+
}
184+
185+
return {
186+
entries: this.cache.size,
187+
size,
188+
}
189+
}
190+
191+
/**
192+
* Validate cache entries and remove stale ones
193+
*/
194+
validate(): number {
195+
if (!this.loaded)
196+
this.load()
197+
198+
let removed = 0
199+
const toRemove: string[] = []
200+
201+
for (const [projectDir, entry] of this.cache.entries()) {
202+
// Check if environment directory still exists
203+
if (!fs.existsSync(entry.envDir)) {
204+
toRemove.push(projectDir)
205+
continue
206+
}
207+
208+
// Check if dependency file mtime has changed
209+
if (entry.depFile && fs.existsSync(entry.depFile)) {
210+
const stats = fs.statSync(entry.depFile)
211+
const currentMtime = Math.floor(stats.mtimeMs / 1000)
212+
if (currentMtime !== entry.depMtime) {
213+
toRemove.push(projectDir)
214+
}
215+
}
216+
}
217+
218+
for (const projectDir of toRemove) {
219+
this.cache.delete(projectDir)
220+
removed++
221+
}
222+
223+
if (removed > 0) {
224+
this.persist()
225+
}
226+
227+
return removed
228+
}
229+
}
230+
231+
// Singleton instance
232+
export const envCache = new EnvCacheManager()
233+
13234
/**
14235
* Load cache metadata
15236
*/
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import type { Command } from '../../cli/types'
2+
3+
const command: Command = {
4+
name: 'benchmark:cache',
5+
description: 'Benchmark cache lookup performance (in-memory vs disk)',
6+
async run({ options }) {
7+
const { runCacheBenchmark } = await import('../../dev/benchmark')
8+
const iterations = typeof options?.iterations === 'string'
9+
? Number.parseInt(options.iterations, 10)
10+
: typeof options?.iterations === 'number'
11+
? options.iterations
12+
: undefined
13+
await runCacheBenchmark({
14+
iterations,
15+
verbose: Boolean(options?.verbose),
16+
json: Boolean(options?.json),
17+
})
18+
return 0
19+
},
20+
}
21+
22+
export default command

packages/launchpad/src/commands/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ const registry: Record<string, () => Promise<Command>> = {
3535
'update': async () => (await import('./update')).default,
3636
'debug:deps': async () => (await import('./debug/deps')).default,
3737
'benchmark:file-detection': async () => (await import('./benchmark/file-detection')).default,
38+
'benchmark:cache': async () => (await import('./benchmark/cache')).default,
3839
'db:create': async () => (await import('./db/create')).default,
3940
// services
4041
'start': async () => (await import('./start')).default,

packages/launchpad/src/dev/benchmark.ts

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -169,6 +169,84 @@ function createTestStructure(baseDir: string, depth: number = 10): string {
169169
return currentPath // Return the deepest directory
170170
}
171171

172+
/**
173+
* Benchmark cache operations
174+
*/
175+
export async function runCacheBenchmark(options: {
176+
iterations?: number
177+
verbose?: boolean
178+
json?: boolean
179+
} = {}): Promise<void> {
180+
const { iterations = 10000, json = false } = options
181+
182+
if (!json) {
183+
console.log('🚀 Cache Performance Benchmark\n')
184+
console.log('Testing in-memory cache lookup performance...\n')
185+
}
186+
187+
// Import cache module
188+
const { envCache } = await import('../cache')
189+
190+
// Populate cache with test data
191+
const testDirs: string[] = []
192+
for (let i = 0; i < 100; i++) {
193+
const dir = `/home/user/projects/test-project-${i}`
194+
testDirs.push(dir)
195+
envCache.set(dir, `${dir}/package.json`, `/home/user/.local/share/launchpad/envs/test-${i}`)
196+
}
197+
198+
const results: Record<string, number> = {}
199+
200+
// Benchmark cache lookups (warm cache)
201+
const lookupTime = await benchmark(
202+
json ? '' : 'Cache lookup (hit) ',
203+
() => {
204+
// Random cache hit
205+
const dir = testDirs[Math.floor(Math.random() * testDirs.length)]
206+
envCache.get(dir)
207+
},
208+
iterations,
209+
)
210+
results['Cache Hit'] = lookupTime
211+
212+
// Benchmark cache misses
213+
const missTime = await benchmark(
214+
json ? '' : 'Cache lookup (miss) ',
215+
() => {
216+
envCache.get('/nonexistent/path/that/does/not/exist')
217+
},
218+
iterations,
219+
)
220+
results['Cache Miss'] = missTime
221+
222+
// Benchmark cache writes
223+
const writeTime = await benchmark(
224+
json ? '' : 'Cache write ',
225+
() => {
226+
const randomDir = `/home/user/projects/bench-${Math.random()}`
227+
envCache.set(randomDir, `${randomDir}/deps.yaml`, `/home/user/.local/share/launchpad/envs/bench-${Math.random()}`)
228+
},
229+
Math.floor(iterations / 10), // Fewer iterations for writes
230+
)
231+
results['Cache Write'] = writeTime
232+
233+
if (json) {
234+
console.log(JSON.stringify(results, null, 2))
235+
}
236+
else {
237+
console.log('\n📊 Cache Performance Summary:')
238+
console.log('─'.repeat(50))
239+
console.log(`Cache Hit: ${lookupTime.toFixed(3)}ms avg`)
240+
console.log(`Cache Miss: ${missTime.toFixed(3)}ms avg`)
241+
console.log(`Cache Write: ${writeTime.toFixed(3)}ms avg`)
242+
console.log(`\n🎯 Target: <0.001ms for cache hits (sub-microsecond)`)
243+
console.log(`✅ Status: ${lookupTime < 0.001 ? 'PASSED' : 'NEEDS IMPROVEMENT'}`)
244+
}
245+
246+
// Cleanup
247+
envCache.clear()
248+
}
249+
172250
/**
173251
* Run file detection benchmark
174252
*/

0 commit comments

Comments
 (0)