11using System . Collections . Concurrent ;
2+ using System . Diagnostics ;
23using Graphify . Export ;
34using Graphify . Graph ;
45using Graphify . Models ;
@@ -33,12 +34,15 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat
3334 {
3435 try
3536 {
37+ var pipelineStopwatch = Stopwatch . StartNew ( ) ;
3638 await WriteLineAsync ( "graphify-dotnet: Transform codebases into knowledge graphs" ) ;
3739 await WriteLineAsync ( new string ( '─' , 60 ) ) ;
40+ await WriteLineAsync ( $ "Started at: { DateTimeOffset . Now : HH:mm:ss} ") ;
3841 await WriteLineAsync ( ) ;
3942
4043 // Stage 1: Detect files
4144 await WriteLineAsync ( "[1/6] Detecting files..." ) ;
45+ var detectStopwatch = Stopwatch . StartNew ( ) ;
4246 var fileDetector = new FileDetector ( ) ;
4347 var detectorOptions = new FileDetectorOptions (
4448 RootPath : inputPath ,
@@ -48,6 +52,7 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat
4852
4953 var detectedFiles = await fileDetector . ExecuteAsync ( detectorOptions , cancellationToken ) ;
5054 await WriteLineAsync ( $ " Found { detectedFiles . Count } files to process") ;
55+ await WriteLineAsync ( $ " Detection completed in { FormatElapsed ( detectStopwatch . Elapsed ) } ") ;
5156 if ( _verbose )
5257 {
5358 foreach ( var file in detectedFiles . Take ( 5 ) )
@@ -68,6 +73,7 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat
6873 int processed = 0 ;
6974 int skipped = 0 ;
7075 int completed = 0 ;
76+ var extractionStopwatch = Stopwatch . StartNew ( ) ;
7177 var verboseWarnings = new ConcurrentQueue < string > ( ) ;
7278 var extractionProgressStep = Math . Max ( 1 , detectedFiles . Count / 10 ) ;
7379
@@ -124,19 +130,22 @@ await Parallel.ForEachAsync(
124130 var totalNodes = extractionResults . Sum ( r => r . Nodes . Count ) ;
125131 var totalEdges = extractionResults . Sum ( r => r . Edges . Count ) ;
126132 await WriteLineAsync ( $ " Extracted { totalNodes } nodes, { totalEdges } edges") ;
133+ await WriteLineAsync ( $ " Extraction completed in { FormatElapsed ( extractionStopwatch . Elapsed ) } ") ;
127134 await WriteLineAsync ( ) ;
128135
129136 // Stage 2b: AI-enhanced semantic extraction (if provider configured)
130137 if ( _chatClient != null )
131138 {
132139 await WriteLineAsync ( "[2b/6] Running AI-enhanced semantic extraction..." ) ;
140+ var semanticStopwatch = Stopwatch . StartNew ( ) ;
133141 var semanticExtractor = new SemanticExtractor ( _chatClient ) ;
134142 int semanticProcessed = 0 ;
135143 int semanticCompleted = 0 ;
136144 var semanticProgressStep = Math . Max ( 1 , detectedFiles . Count / 10 ) ;
137145
138- foreach ( var file in detectedFiles )
146+ for ( var index = 0 ; index < detectedFiles . Count ; index ++ )
139147 {
148+ var file = detectedFiles [ index ] ;
140149 cancellationToken . ThrowIfCancellationRequested ( ) ;
141150 try
142151 {
@@ -164,6 +173,7 @@ await Parallel.ForEachAsync(
164173 totalNodes = extractionResults . Sum ( r => r . Nodes . Count ) ;
165174 totalEdges = extractionResults . Sum ( r => r . Edges . Count ) ;
166175 await WriteLineAsync ( $ " Total: { totalNodes } nodes, { totalEdges } edges (AST + AI)") ;
176+ await WriteLineAsync ( $ " Semantic extraction completed in { FormatElapsed ( semanticStopwatch . Elapsed ) } ") ;
167177 await WriteLineAsync ( ) ;
168178 }
169179 else
@@ -175,6 +185,7 @@ await Parallel.ForEachAsync(
175185
176186 // Stage 3: Build graph
177187 await WriteLineAsync ( "[3/6] Building knowledge graph..." ) ;
188+ var graphBuildStopwatch = Stopwatch . StartNew ( ) ;
178189 var graphBuilder = new GraphBuilder ( new GraphBuilderOptions
179190 {
180191 CreateFileNodes = true ,
@@ -183,10 +194,12 @@ await Parallel.ForEachAsync(
183194 } ) ;
184195 var graph = await graphBuilder . ExecuteAsync ( extractionResults , cancellationToken ) ;
185196 await WriteLineAsync ( $ " Graph: { graph . NodeCount } nodes, { graph . EdgeCount } edges") ;
197+ await WriteLineAsync ( $ " Graph build completed in { FormatElapsed ( graphBuildStopwatch . Elapsed ) } ") ;
186198 await WriteLineAsync ( ) ;
187199
188200 // Stage 4: Detect communities (clustering)
189201 await WriteLineAsync ( "[4/6] Detecting communities..." ) ;
202+ var clusterStopwatch = Stopwatch . StartNew ( ) ;
190203 var clusterEngine = new ClusterEngine ( new ClusterOptions
191204 {
192205 MaxIterations = 100 ,
@@ -201,10 +214,12 @@ await Parallel.ForEachAsync(
201214 . Distinct ( )
202215 . Count ( ) ;
203216 await WriteLineAsync ( $ " Found { communityCount } communities") ;
217+ await WriteLineAsync ( $ " Clustering completed in { FormatElapsed ( clusterStopwatch . Elapsed ) } ") ;
204218 await WriteLineAsync ( ) ;
205219
206220 // Stage 5: Analyze graph
207221 await WriteLineAsync ( "[5/6] Analyzing graph structure..." ) ;
222+ var analysisStopwatch = Stopwatch . StartNew ( ) ;
208223 var analyzer = new Analyzer ( new AnalyzerOptions
209224 {
210225 TopGodNodesCount = 10 ,
@@ -215,6 +230,7 @@ await Parallel.ForEachAsync(
215230 await WriteLineAsync ( $ " God nodes: { analysis . GodNodes . Count } ") ;
216231 await WriteLineAsync ( $ " Surprising connections: { analysis . SurprisingConnections . Count } ") ;
217232 await WriteLineAsync ( $ " Suggested questions: { analysis . SuggestedQuestions . Count } ") ;
233+ await WriteLineAsync ( $ " Analysis completed in { FormatElapsed ( analysisStopwatch . Elapsed ) } ") ;
218234 await WriteLineAsync ( ) ;
219235
220236 // Prepare community labels and cohesion scores for report and exports
@@ -223,6 +239,7 @@ await Parallel.ForEachAsync(
223239
224240 // Stage 6: Export
225241 await WriteLineAsync ( "[6/6] Exporting results..." ) ;
242+ var exportStopwatch = Stopwatch . StartNew ( ) ;
226243
227244 // Validate output directory to prevent path traversal
228245 var validator = new Graphify . Security . InputValidator ( ) ;
@@ -238,6 +255,7 @@ await Parallel.ForEachAsync(
238255 {
239256 try
240257 {
258+ var formatStopwatch = Stopwatch . StartNew ( ) ;
241259 var normalizedFormat = format . ToLowerInvariant ( ) ;
242260
243261 switch ( normalizedFormat )
@@ -246,49 +264,49 @@ await Parallel.ForEachAsync(
246264 var jsonExporter = new JsonExporter ( ) ;
247265 var jsonPath = Path . Combine ( outputDir , "graph.json" ) ;
248266 await jsonExporter . ExportAsync ( graph , jsonPath , cancellationToken ) ;
249- await WriteLineAsync ( $ " Exported JSON: { jsonPath } ") ;
267+ await WriteLineAsync ( $ " Exported JSON: { jsonPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
250268 break ;
251269
252270 case "html" :
253271 var htmlExporter = new HtmlExporter ( ) ;
254272 var htmlPath = Path . Combine ( outputDir , "graph.html" ) ;
255273 await htmlExporter . ExportAsync ( graph , htmlPath , communityLabels , cancellationToken ) ;
256- await WriteLineAsync ( $ " Exported HTML: { htmlPath } ") ;
274+ await WriteLineAsync ( $ " Exported HTML: { htmlPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
257275 break ;
258276
259277 case "svg" :
260278 var svgExporter = new SvgExporter ( ) ;
261279 var svgPath = Path . Combine ( outputDir , "graph.svg" ) ;
262280 await svgExporter . ExportAsync ( graph , svgPath , cancellationToken ) ;
263- await WriteLineAsync ( $ " Exported SVG: { svgPath } ") ;
281+ await WriteLineAsync ( $ " Exported SVG: { svgPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
264282 break ;
265283
266284 case "neo4j" :
267285 var neo4jExporter = new Neo4jExporter ( ) ;
268286 var cypherPath = Path . Combine ( outputDir , "graph.cypher" ) ;
269287 await neo4jExporter . ExportAsync ( graph , cypherPath , cancellationToken ) ;
270- await WriteLineAsync ( $ " Exported Neo4j Cypher: { cypherPath } ") ;
288+ await WriteLineAsync ( $ " Exported Neo4j Cypher: { cypherPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
271289 break ;
272290
273291 case "ladybug" :
274292 var ladybugExporter = new LadybugExporter ( ) ;
275293 var ladybugPath = Path . Combine ( outputDir , "graph.ladybug.cypher" ) ;
276294 await ladybugExporter . ExportAsync ( graph , ladybugPath , cancellationToken ) ;
277- await WriteLineAsync ( $ " Exported Ladybug Cypher: { ladybugPath } ") ;
295+ await WriteLineAsync ( $ " Exported Ladybug Cypher: { ladybugPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
278296 break ;
279297
280298 case "obsidian" :
281299 var obsidianExporter = new ObsidianExporter ( ) ;
282300 var obsidianPath = Path . Combine ( outputDir , "obsidian" ) ;
283301 await obsidianExporter . ExportAsync ( graph , obsidianPath , cancellationToken ) ;
284- await WriteLineAsync ( $ " Exported Obsidian vault: { obsidianPath } /") ;
302+ await WriteLineAsync ( $ " Exported Obsidian vault: { obsidianPath } /{ FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
285303 break ;
286304
287305 case "wiki" :
288306 var wikiExporter = new WikiExporter ( ) ;
289307 var wikiPath = Path . Combine ( outputDir , "wiki" ) ;
290308 await wikiExporter . ExportAsync ( graph , wikiPath , cancellationToken ) ;
291- await WriteLineAsync ( $ " Exported Wiki: { wikiPath } /") ;
309+ await WriteLineAsync ( $ " Exported Wiki: { wikiPath } /{ FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
292310 break ;
293311
294312 case "report" :
@@ -297,7 +315,7 @@ await Parallel.ForEachAsync(
297315 var reportMarkdown = reportGenerator . Generate ( graph , analysis , communityLabels , cohesionScores , projectName ) ;
298316 var reportPath = Path . Combine ( outputDir , "GRAPH_REPORT.md" ) ;
299317 await File . WriteAllTextAsync ( reportPath , reportMarkdown , cancellationToken ) ;
300- await WriteLineAsync ( $ " Exported Report: { reportPath } ") ;
318+ await WriteLineAsync ( $ " Exported Report: { reportPath } { FormatWithElapsed ( formatStopwatch . Elapsed ) } ") ;
301319 break ;
302320
303321 default :
@@ -310,9 +328,11 @@ await Parallel.ForEachAsync(
310328 await WriteLineAsync ( $ " Error exporting { format } : { ex . Message } ") ;
311329 }
312330 }
331+ await WriteLineAsync ( $ " Export stage completed in { FormatElapsed ( exportStopwatch . Elapsed ) } ") ;
313332
314333 await WriteLineAsync ( ) ;
315334 await WriteLineAsync ( "✓ Pipeline completed successfully" ) ;
335+ await WriteLineAsync ( $ "Total runtime: { FormatElapsed ( pipelineStopwatch . Elapsed ) } ") ;
316336 await WriteLineAsync ( ) ;
317337
318338 // Print summary
@@ -371,6 +391,18 @@ private static bool ShouldReportProgress(int completed, int total, int step)
371391 return completed == total || completed % step == 0 ;
372392 }
373393
394+ private static string FormatElapsed ( TimeSpan elapsed )
395+ {
396+ return elapsed . TotalSeconds >= 1
397+ ? $ "{ elapsed . TotalSeconds : F2} s"
398+ : $ "{ elapsed . TotalMilliseconds : F0} ms";
399+ }
400+
401+ private string FormatWithElapsed ( TimeSpan elapsed )
402+ {
403+ return _verbose ? $ " ({ FormatElapsed ( elapsed ) } )" : string . Empty ;
404+ }
405+
374406 private static Dictionary < int , string > BuildCommunityLabels ( KnowledgeGraph graph )
375407 {
376408 var result = new Dictionary < int , string > ( ) ;
0 commit comments