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