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+ }