1+ import * as vscode from "vscode"
2+ import * as os from "os"
3+ import * as path from "path"
4+ import * as fs from "fs/promises"
5+
6+ /**
7+ * A class for tracking memory usage and logging resource lifecycles
8+ * to help identify memory leaks in the extension.
9+ */
10+ export class MemoryLogger {
11+ private outputChannel : vscode . OutputChannel
12+ private logFile : string | undefined
13+ private resourceCounts = new Map < string , number > ( )
14+ private startTime : number = Date . now ( )
15+ private logInterval : NodeJS . Timeout | undefined
16+ private disposed : boolean = false
17+
18+ constructor ( ) {
19+ this . outputChannel = vscode . window . createOutputChannel ( "Coder Memory Logging" )
20+ this . outputChannel . show ( )
21+
22+ // Setup periodic logging of memory usage
23+ this . startPeriodicLogging ( )
24+ }
25+
26+ /**
27+ * Start logging memory usage periodically
28+ */
29+ private startPeriodicLogging ( intervalMs = 60000 ) {
30+ if ( this . logInterval ) {
31+ clearInterval ( this . logInterval )
32+ }
33+
34+ this . logInterval = setInterval ( ( ) => {
35+ if ( this . disposed ) return
36+ this . logMemoryUsage ( "PERIODIC" )
37+ this . logResourceCounts ( )
38+ } , intervalMs )
39+ }
40+
41+ /**
42+ * Initialize the log file for persistent logging
43+ */
44+ public async initLogFile ( globalStoragePath : string ) : Promise < void > {
45+ try {
46+ const logDir = path . join ( globalStoragePath , "logs" )
47+ await fs . mkdir ( logDir , { recursive : true } )
48+
49+ this . logFile = path . join ( logDir , `memory-log-${ new Date ( ) . toISOString ( ) . replace ( / [: .] / g, "-" ) } .txt` )
50+
51+ await this . writeToLogFile ( "Memory logging initialized" )
52+ this . info ( "Memory logging initialized to file: " + this . logFile )
53+
54+ // Log initial memory state
55+ this . logMemoryUsage ( "INIT" )
56+ } catch ( err ) {
57+ this . error ( `Failed to initialize log file: ${ err } ` )
58+ }
59+ }
60+
61+ /**
62+ * Log a new resource creation
63+ */
64+ public trackResourceCreated ( resourceType : string , id : string = "" ) : void {
65+ const count = ( this . resourceCounts . get ( resourceType ) || 0 ) + 1
66+ this . resourceCounts . set ( resourceType , count )
67+ this . info ( `RESOURCE_CREATED: ${ resourceType } ${ id ? ":" + id : "" } (Total: ${ count } )` )
68+ }
69+
70+ /**
71+ * Log a resource disposal
72+ */
73+ public trackResourceDisposed ( resourceType : string , id : string = "" ) : void {
74+ const count = Math . max ( 0 , ( this . resourceCounts . get ( resourceType ) || 1 ) - 1 )
75+ if ( count === 0 ) {
76+ this . resourceCounts . delete ( resourceType )
77+ } else {
78+ this . resourceCounts . set ( resourceType , count )
79+ }
80+
81+ this . info ( `RESOURCE_DISPOSED: ${ resourceType } ${ id ? ":" + id : "" } (Remaining: ${ count } )` )
82+ }
83+
84+ /**
85+ * Log error with memory usage
86+ */
87+ public error ( message : string , error ?: unknown ) : void {
88+ const errorMsg = error ? `: ${ error instanceof Error ? error . stack || error . message : String ( error ) } ` : ""
89+ const fullMessage = `[ERROR] ${ message } ${ errorMsg } `
90+
91+ this . outputChannel . appendLine ( fullMessage )
92+ this . writeToLogFile ( fullMessage )
93+ this . logMemoryUsage ( "ERROR" )
94+ }
95+
96+ /**
97+ * Log info with timestamp
98+ */
99+ public info ( message : string ) : void {
100+ const fullMessage = `[INFO] ${ message } `
101+ this . outputChannel . appendLine ( fullMessage )
102+ this . writeToLogFile ( fullMessage )
103+ }
104+
105+ /**
106+ * Log debug info (only to file)
107+ */
108+ public debug ( message : string ) : void {
109+ const fullMessage = `[DEBUG] ${ message } `
110+ this . writeToLogFile ( fullMessage )
111+ }
112+
113+ /**
114+ * Log current memory usage
115+ */
116+ public logMemoryUsage ( context : string ) : void {
117+ try {
118+ const memoryUsage = process . memoryUsage ( )
119+ const nodeMemoryInfo = {
120+ rss : `${ ( memoryUsage . rss / 1024 / 1024 ) . toFixed ( 2 ) } MB` ,
121+ heapTotal : `${ ( memoryUsage . heapTotal / 1024 / 1024 ) . toFixed ( 2 ) } MB` ,
122+ heapUsed : `${ ( memoryUsage . heapUsed / 1024 / 1024 ) . toFixed ( 2 ) } MB` ,
123+ external : `${ ( memoryUsage . external / 1024 / 1024 ) . toFixed ( 2 ) } MB` ,
124+ uptime : formatDuration ( process . uptime ( ) * 1000 ) ,
125+ totalUptime : formatDuration ( Date . now ( ) - this . startTime )
126+ }
127+
128+ const systemMemoryInfo = {
129+ totalMem : `${ ( os . totalmem ( ) / 1024 / 1024 / 1024 ) . toFixed ( 2 ) } GB` ,
130+ freeMem : `${ ( os . freemem ( ) / 1024 / 1024 / 1024 ) . toFixed ( 2 ) } GB` ,
131+ loadAvg : os . loadavg ( ) . map ( load => load . toFixed ( 2 ) ) . join ( ", " )
132+ }
133+
134+ const memoryLog = `[MEMORY:${ context } ] Node: ${ JSON . stringify ( nodeMemoryInfo ) } | System: ${ JSON . stringify ( systemMemoryInfo ) } `
135+ this . outputChannel . appendLine ( memoryLog )
136+ this . writeToLogFile ( memoryLog )
137+ } catch ( err ) {
138+ this . outputChannel . appendLine ( `[ERROR] Failed to log memory usage: ${ err } ` )
139+ }
140+ }
141+
142+ /**
143+ * Log the current counts of active resources
144+ */
145+ private logResourceCounts ( ) : void {
146+ const counts = Array . from ( this . resourceCounts . entries ( ) )
147+ . map ( ( [ type , count ] ) => `${ type } =${ count } ` )
148+ . join ( ", " )
149+
150+ const message = `[RESOURCES] Active resources: ${ counts || "none" } `
151+ this . outputChannel . appendLine ( message )
152+ this . writeToLogFile ( message )
153+ }
154+
155+ /**
156+ * Write to log file
157+ */
158+ private async writeToLogFile ( message : string ) : Promise < void > {
159+ if ( ! this . logFile ) return
160+
161+ try {
162+ const timestamp = new Date ( ) . toISOString ( )
163+ await fs . appendFile ( this . logFile , `${ timestamp } ${ message } \n` )
164+ } catch ( err ) {
165+ // Don't recursively call this.error to avoid potential loops
166+ this . outputChannel . appendLine ( `[ERROR] Failed to write to log file: ${ err } ` )
167+ }
168+ }
169+
170+ /**
171+ * Show the log in the output channel
172+ */
173+ public show ( ) : void {
174+ this . outputChannel . show ( )
175+ }
176+
177+ /**
178+ * Dispose of the logger
179+ */
180+ public dispose ( ) : void {
181+ this . disposed = true
182+ if ( this . logInterval ) {
183+ clearInterval ( this . logInterval )
184+ this . logInterval = undefined
185+ }
186+ this . logMemoryUsage ( "DISPOSE" )
187+ this . outputChannel . dispose ( )
188+ }
189+ }
190+
191+ /**
192+ * Format duration in milliseconds to a human-readable string
193+ */
194+ function formatDuration ( ms : number ) : string {
195+ const seconds = Math . floor ( ( ms / 1000 ) % 60 )
196+ const minutes = Math . floor ( ( ms / ( 1000 * 60 ) ) % 60 )
197+ const hours = Math . floor ( ( ms / ( 1000 * 60 * 60 ) ) % 24 )
198+ const days = Math . floor ( ms / ( 1000 * 60 * 60 * 24 ) )
199+
200+ return `${ days } d ${ hours } h ${ minutes } m ${ seconds } s`
201+ }
202+
203+ // Singleton instance
204+ let instance : MemoryLogger | undefined
205+
206+ /**
207+ * Get or initialize the memory logger instance
208+ */
209+ export function getMemoryLogger ( ) : MemoryLogger {
210+ if ( ! instance ) {
211+ instance = new MemoryLogger ( )
212+ }
213+ return instance
214+ }
0 commit comments