11using System . Collections . Concurrent ;
2+ using System . Diagnostics ;
23using Graphify . Export ;
34using Graphify . Graph ;
45using Graphify . Models ;
@@ -32,12 +33,15 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat
3233 {
3334 try
3435 {
36+ var pipelineStopwatch = Stopwatch . StartNew ( ) ;
3537 await WriteLineAsync ( "graphify-dotnet: Transform codebases into knowledge graphs" ) ;
3638 await WriteLineAsync ( new string ( '─' , 60 ) ) ;
39+ await WriteLineAsync ( $ "Started at: { DateTimeOffset . Now : HH:mm:ss} ") ;
3740 await WriteLineAsync ( ) ;
3841
3942 // Stage 1: Detect files
4043 await WriteLineAsync ( "[1/6] Detecting files..." ) ;
44+ var detectStopwatch = Stopwatch . StartNew ( ) ;
4145 var fileDetector = new FileDetector ( ) ;
4246 var detectorOptions = new FileDetectorOptions (
4347 RootPath : inputPath ,
@@ -47,6 +51,7 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat
4751
4852 var detectedFiles = await fileDetector . ExecuteAsync ( detectorOptions , cancellationToken ) ;
4953 await WriteLineAsync ( $ " Found { detectedFiles . Count } files to process") ;
54+ await WriteLineAsync ( $ " Detection completed in { FormatElapsed ( detectStopwatch . Elapsed ) } ") ;
5055 if ( _verbose )
5156 {
5257 foreach ( var file in detectedFiles . Take ( 5 ) )
@@ -66,6 +71,9 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat
6671 var extractionBag = new ConcurrentBag < ExtractionResult > ( ) ;
6772 int processed = 0 ;
6873 int skipped = 0 ;
74+ int inspected = 0 ;
75+ var extractionStopwatch = Stopwatch . StartNew ( ) ;
76+ var extractionProgressInterval = Math . Max ( 10 , detectedFiles . Count / 10 ) ;
6977 var verboseWarnings = new ConcurrentQueue < string > ( ) ;
7078
7179 await Parallel . ForEachAsync (
@@ -98,6 +106,17 @@ await Parallel.ForEachAsync(
98106 verboseWarnings . Enqueue ( $ " Warning: Failed to extract { file . RelativePath } : { ex . Message } ") ;
99107 }
100108 }
109+ finally
110+ {
111+ var currentInspected = Interlocked . Increment ( ref inspected ) ;
112+ if ( _verbose &&
113+ detectedFiles . Count > 0 &&
114+ currentInspected % extractionProgressInterval == 0 )
115+ {
116+ verboseWarnings . Enqueue (
117+ $ " Progress: { currentInspected } /{ detectedFiles . Count } files inspected ({ processed } extracted, { skipped } skipped)") ;
118+ }
119+ }
101120 } ) ;
102121
103122 var extractionResults = new List < ExtractionResult > ( extractionBag ) ;
@@ -110,17 +129,21 @@ await Parallel.ForEachAsync(
110129 var totalNodes = extractionResults . Sum ( r => r . Nodes . Count ) ;
111130 var totalEdges = extractionResults . Sum ( r => r . Edges . Count ) ;
112131 await WriteLineAsync ( $ " Extracted { totalNodes } nodes, { totalEdges } edges") ;
132+ await WriteLineAsync ( $ " Extraction completed in { FormatElapsed ( extractionStopwatch . Elapsed ) } ") ;
113133 await WriteLineAsync ( ) ;
114134
115135 // Stage 2b: AI-enhanced semantic extraction (if provider configured)
116136 if ( _chatClient != null )
117137 {
118138 await WriteLineAsync ( "[2b/6] Running AI-enhanced semantic extraction..." ) ;
139+ var semanticStopwatch = Stopwatch . StartNew ( ) ;
119140 var semanticExtractor = new SemanticExtractor ( _chatClient ) ;
120141 int semanticProcessed = 0 ;
142+ var semanticProgressInterval = Math . Max ( 5 , detectedFiles . Count / 10 ) ;
121143
122- foreach ( var file in detectedFiles )
144+ for ( var index = 0 ; index < detectedFiles . Count ; index ++ )
123145 {
146+ var file = detectedFiles [ index ] ;
124147 cancellationToken . ThrowIfCancellationRequested ( ) ;
125148 try
126149 {
@@ -136,12 +159,21 @@ await Parallel.ForEachAsync(
136159 if ( _verbose )
137160 await WriteLineAsync ( $ " Warning: Semantic extraction failed for { file . RelativePath } : { ex . Message } ") ;
138161 }
162+
163+ if ( _verbose &&
164+ detectedFiles . Count > 0 &&
165+ ( index + 1 ) % semanticProgressInterval == 0 )
166+ {
167+ await WriteLineAsync (
168+ $ " Progress: { index + 1 } /{ detectedFiles . Count } semantic files analyzed ({ semanticProcessed } produced output) in { FormatElapsed ( semanticStopwatch . Elapsed ) } ") ;
169+ }
139170 }
140171
141172 await WriteLineAsync ( $ " AI extracted from { semanticProcessed } files") ;
142173 totalNodes = extractionResults . Sum ( r => r . Nodes . Count ) ;
143174 totalEdges = extractionResults . Sum ( r => r . Edges . Count ) ;
144175 await WriteLineAsync ( $ " Total: { totalNodes } nodes, { totalEdges } edges (AST + AI)") ;
176+ await WriteLineAsync ( $ " Semantic extraction completed in { FormatElapsed ( semanticStopwatch . Elapsed ) } ") ;
145177 await WriteLineAsync ( ) ;
146178 }
147179 else
@@ -153,6 +185,7 @@ await Parallel.ForEachAsync(
153185
154186 // Stage 3: Build graph
155187 await WriteLineAsync ( "[3/6] Building knowledge graph..." ) ;
188+ var graphBuildStopwatch = Stopwatch . StartNew ( ) ;
156189 var graphBuilder = new GraphBuilder ( new GraphBuilderOptions
157190 {
158191 CreateFileNodes = true ,
@@ -161,10 +194,12 @@ await Parallel.ForEachAsync(
161194 } ) ;
162195 var graph = await graphBuilder . ExecuteAsync ( extractionResults , cancellationToken ) ;
163196 await WriteLineAsync ( $ " Graph: { graph . NodeCount } nodes, { graph . EdgeCount } edges") ;
197+ await WriteLineAsync ( $ " Graph build completed in { FormatElapsed ( graphBuildStopwatch . Elapsed ) } ") ;
164198 await WriteLineAsync ( ) ;
165199
166200 // Stage 4: Detect communities (clustering)
167201 await WriteLineAsync ( "[4/6] Detecting communities..." ) ;
202+ var clusterStopwatch = Stopwatch . StartNew ( ) ;
168203 var clusterEngine = new ClusterEngine ( new ClusterOptions
169204 {
170205 MaxIterations = 100 ,
@@ -179,10 +214,12 @@ await Parallel.ForEachAsync(
179214 . Distinct ( )
180215 . Count ( ) ;
181216 await WriteLineAsync ( $ " Found { communityCount } communities") ;
217+ await WriteLineAsync ( $ " Clustering completed in { FormatElapsed ( clusterStopwatch . Elapsed ) } ") ;
182218 await WriteLineAsync ( ) ;
183219
184220 // Stage 5: Analyze graph
185221 await WriteLineAsync ( "[5/6] Analyzing graph structure..." ) ;
222+ var analysisStopwatch = Stopwatch . StartNew ( ) ;
186223 var analyzer = new Analyzer ( new AnalyzerOptions
187224 {
188225 TopGodNodesCount = 10 ,
@@ -193,6 +230,7 @@ await Parallel.ForEachAsync(
193230 await WriteLineAsync ( $ " God nodes: { analysis . GodNodes . Count } ") ;
194231 await WriteLineAsync ( $ " Surprising connections: { analysis . SurprisingConnections . Count } ") ;
195232 await WriteLineAsync ( $ " Suggested questions: { analysis . SuggestedQuestions . Count } ") ;
233+ await WriteLineAsync ( $ " Analysis completed in { FormatElapsed ( analysisStopwatch . Elapsed ) } ") ;
196234 await WriteLineAsync ( ) ;
197235
198236 // Prepare community labels and cohesion scores for report and exports
@@ -201,6 +239,7 @@ await Parallel.ForEachAsync(
201239
202240 // Stage 6: Export
203241 await WriteLineAsync ( "[6/6] Exporting results..." ) ;
242+ var exportStopwatch = Stopwatch . StartNew ( ) ;
204243
205244 // Validate output directory to prevent path traversal
206245 var validator = new Graphify . Security . InputValidator ( ) ;
@@ -216,6 +255,7 @@ await Parallel.ForEachAsync(
216255 {
217256 try
218257 {
258+ var formatStopwatch = Stopwatch . StartNew ( ) ;
219259 var normalizedFormat = format . ToLowerInvariant ( ) ;
220260
221261 switch ( normalizedFormat )
@@ -224,49 +264,49 @@ await Parallel.ForEachAsync(
224264 var jsonExporter = new JsonExporter ( ) ;
225265 var jsonPath = Path . Combine ( outputDir , "graph.json" ) ;
226266 await jsonExporter . ExportAsync ( graph , jsonPath , cancellationToken ) ;
227- await WriteLineAsync ( $ " Exported JSON: { jsonPath } ") ;
267+ await WriteLineAsync ( $ " Exported JSON: { jsonPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
228268 break ;
229269
230270 case "html" :
231271 var htmlExporter = new HtmlExporter ( ) ;
232272 var htmlPath = Path . Combine ( outputDir , "graph.html" ) ;
233273 await htmlExporter . ExportAsync ( graph , htmlPath , communityLabels , cancellationToken ) ;
234- await WriteLineAsync ( $ " Exported HTML: { htmlPath } ") ;
274+ await WriteLineAsync ( $ " Exported HTML: { htmlPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
235275 break ;
236276
237277 case "svg" :
238278 var svgExporter = new SvgExporter ( ) ;
239279 var svgPath = Path . Combine ( outputDir , "graph.svg" ) ;
240280 await svgExporter . ExportAsync ( graph , svgPath , cancellationToken ) ;
241- await WriteLineAsync ( $ " Exported SVG: { svgPath } ") ;
281+ await WriteLineAsync ( $ " Exported SVG: { svgPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
242282 break ;
243283
244284 case "neo4j" :
245285 var neo4jExporter = new Neo4jExporter ( ) ;
246286 var cypherPath = Path . Combine ( outputDir , "graph.cypher" ) ;
247287 await neo4jExporter . ExportAsync ( graph , cypherPath , cancellationToken ) ;
248- await WriteLineAsync ( $ " Exported Neo4j Cypher: { cypherPath } ") ;
288+ await WriteLineAsync ( $ " Exported Neo4j Cypher: { cypherPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
249289 break ;
250290
251291 case "ladybug" :
252292 var ladybugExporter = new LadybugExporter ( ) ;
253293 var ladybugPath = Path . Combine ( outputDir , "graph.ladybug.cypher" ) ;
254294 await ladybugExporter . ExportAsync ( graph , ladybugPath , cancellationToken ) ;
255- await WriteLineAsync ( $ " Exported Ladybug Cypher: { ladybugPath } ") ;
295+ await WriteLineAsync ( $ " Exported Ladybug Cypher: { ladybugPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
256296 break ;
257297
258298 case "obsidian" :
259299 var obsidianExporter = new ObsidianExporter ( ) ;
260300 var obsidianPath = Path . Combine ( outputDir , "obsidian" ) ;
261301 await obsidianExporter . ExportAsync ( graph , obsidianPath , cancellationToken ) ;
262- await WriteLineAsync ( $ " Exported Obsidian vault: { obsidianPath } /") ;
302+ await WriteLineAsync ( $ " Exported Obsidian vault: { obsidianPath } /{ FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
263303 break ;
264304
265305 case "wiki" :
266306 var wikiExporter = new WikiExporter ( ) ;
267307 var wikiPath = Path . Combine ( outputDir , "wiki" ) ;
268308 await wikiExporter . ExportAsync ( graph , wikiPath , cancellationToken ) ;
269- await WriteLineAsync ( $ " Exported Wiki: { wikiPath } /") ;
309+ await WriteLineAsync ( $ " Exported Wiki: { wikiPath } /{ FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
270310 break ;
271311
272312 case "report" :
@@ -275,7 +315,7 @@ await Parallel.ForEachAsync(
275315 var reportMarkdown = reportGenerator . Generate ( graph , analysis , communityLabels , cohesionScores , projectName ) ;
276316 var reportPath = Path . Combine ( outputDir , "GRAPH_REPORT.md" ) ;
277317 await File . WriteAllTextAsync ( reportPath , reportMarkdown , cancellationToken ) ;
278- await WriteLineAsync ( $ " Exported Report: { reportPath } ") ;
318+ await WriteLineAsync ( $ " Exported Report: { reportPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
279319 break ;
280320
281321 default :
@@ -288,9 +328,11 @@ await Parallel.ForEachAsync(
288328 await WriteLineAsync ( $ " Error exporting { format } : { ex . Message } ") ;
289329 }
290330 }
331+ await WriteLineAsync ( $ " Export stage completed in { FormatElapsed ( exportStopwatch . Elapsed ) } ") ;
291332
292333 await WriteLineAsync ( ) ;
293334 await WriteLineAsync ( "✓ Pipeline completed successfully" ) ;
335+ await WriteLineAsync ( $ "Total runtime: { FormatElapsed ( pipelineStopwatch . Elapsed ) } ") ;
294336 await WriteLineAsync ( ) ;
295337
296338 // Print summary
@@ -336,6 +378,18 @@ private async Task WriteLineAsync(string message = "")
336378 await _output . WriteLineAsync ( message ) ;
337379 }
338380
381+ private static string FormatElapsed ( TimeSpan elapsed )
382+ {
383+ return elapsed . TotalSeconds >= 1
384+ ? $ "{ elapsed . TotalSeconds : F2} s"
385+ : $ "{ elapsed . TotalMilliseconds : F0} ms";
386+ }
387+
388+ private string FormatWithElapsed ( TimeSpan elapsed )
389+ {
390+ return _verbose ? $ " ({ FormatElapsed ( elapsed ) } )" : string . Empty ;
391+ }
392+
339393 private static Dictionary < int , string > BuildCommunityLabels ( KnowledgeGraph graph )
340394 {
341395 var result = new Dictionary < int , string > ( ) ;
0 commit comments