diff --git a/src/Graphify.Cli/PipelineRunner.cs b/src/Graphify.Cli/PipelineRunner.cs index b960101..af07154 100644 --- a/src/Graphify.Cli/PipelineRunner.cs +++ b/src/Graphify.Cli/PipelineRunner.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics; using Graphify.Export; using Graphify.Graph; using Graphify.Models; @@ -33,12 +34,15 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat { try { + var pipelineStopwatch = Stopwatch.StartNew(); await WriteLineAsync("graphify-dotnet: Transform codebases into knowledge graphs"); await WriteLineAsync(new string('─', 60)); + await WriteLineAsync($"Started at: {DateTimeOffset.Now:HH:mm:ss}"); await WriteLineAsync(); // Stage 1: Detect files await WriteLineAsync("[1/6] Detecting files..."); + var detectStopwatch = Stopwatch.StartNew(); var fileDetector = new FileDetector(); var detectorOptions = new FileDetectorOptions( RootPath: inputPath, @@ -48,6 +52,7 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat var detectedFiles = await fileDetector.ExecuteAsync(detectorOptions, cancellationToken); await WriteLineAsync($" Found {detectedFiles.Count} files to process"); + await WriteLineAsync($" Detection completed in {FormatElapsed(detectStopwatch.Elapsed)}"); if (_verbose) { foreach (var file in detectedFiles.Take(5)) @@ -68,6 +73,7 @@ public PipelineRunner(TextWriter output, bool verbose = false, IChatClient? chat int processed = 0; int skipped = 0; int completed = 0; + var extractionStopwatch = Stopwatch.StartNew(); var verboseWarnings = new ConcurrentQueue(); var extractionProgressStep = Math.Max(1, detectedFiles.Count / 10); @@ -124,19 +130,22 @@ await Parallel.ForEachAsync( var totalNodes = extractionResults.Sum(r => r.Nodes.Count); var totalEdges = extractionResults.Sum(r => r.Edges.Count); await WriteLineAsync($" Extracted {totalNodes} nodes, {totalEdges} edges"); + await WriteLineAsync($" Extraction completed in {FormatElapsed(extractionStopwatch.Elapsed)}"); await WriteLineAsync(); // Stage 2b: AI-enhanced semantic extraction (if provider configured) if (_chatClient != null) { await WriteLineAsync("[2b/6] Running AI-enhanced semantic extraction..."); + var semanticStopwatch = Stopwatch.StartNew(); var semanticExtractor = new SemanticExtractor(_chatClient); int semanticProcessed = 0; int semanticCompleted = 0; var semanticProgressStep = Math.Max(1, detectedFiles.Count / 10); - foreach (var file in detectedFiles) + for (var index = 0; index < detectedFiles.Count; index++) { + var file = detectedFiles[index]; cancellationToken.ThrowIfCancellationRequested(); try { @@ -164,6 +173,7 @@ await Parallel.ForEachAsync( totalNodes = extractionResults.Sum(r => r.Nodes.Count); totalEdges = extractionResults.Sum(r => r.Edges.Count); await WriteLineAsync($" Total: {totalNodes} nodes, {totalEdges} edges (AST + AI)"); + await WriteLineAsync($" Semantic extraction completed in {FormatElapsed(semanticStopwatch.Elapsed)}"); await WriteLineAsync(); } else @@ -175,6 +185,7 @@ await Parallel.ForEachAsync( // Stage 3: Build graph await WriteLineAsync("[3/6] Building knowledge graph..."); + var graphBuildStopwatch = Stopwatch.StartNew(); var graphBuilder = new GraphBuilder(new GraphBuilderOptions { CreateFileNodes = true, @@ -183,10 +194,12 @@ await Parallel.ForEachAsync( }); var graph = await graphBuilder.ExecuteAsync(extractionResults, cancellationToken); await WriteLineAsync($" Graph: {graph.NodeCount} nodes, {graph.EdgeCount} edges"); + await WriteLineAsync($" Graph build completed in {FormatElapsed(graphBuildStopwatch.Elapsed)}"); await WriteLineAsync(); // Stage 4: Detect communities (clustering) await WriteLineAsync("[4/6] Detecting communities..."); + var clusterStopwatch = Stopwatch.StartNew(); var clusterEngine = new ClusterEngine(new ClusterOptions { MaxIterations = 100, @@ -201,10 +214,12 @@ await Parallel.ForEachAsync( .Distinct() .Count(); await WriteLineAsync($" Found {communityCount} communities"); + await WriteLineAsync($" Clustering completed in {FormatElapsed(clusterStopwatch.Elapsed)}"); await WriteLineAsync(); // Stage 5: Analyze graph await WriteLineAsync("[5/6] Analyzing graph structure..."); + var analysisStopwatch = Stopwatch.StartNew(); var analyzer = new Analyzer(new AnalyzerOptions { TopGodNodesCount = 10, @@ -215,6 +230,7 @@ await Parallel.ForEachAsync( await WriteLineAsync($" God nodes: {analysis.GodNodes.Count}"); await WriteLineAsync($" Surprising connections: {analysis.SurprisingConnections.Count}"); await WriteLineAsync($" Suggested questions: {analysis.SuggestedQuestions.Count}"); + await WriteLineAsync($" Analysis completed in {FormatElapsed(analysisStopwatch.Elapsed)}"); await WriteLineAsync(); // Prepare community labels and cohesion scores for report and exports @@ -223,6 +239,7 @@ await Parallel.ForEachAsync( // Stage 6: Export await WriteLineAsync("[6/6] Exporting results..."); + var exportStopwatch = Stopwatch.StartNew(); // Validate output directory to prevent path traversal var validator = new Graphify.Security.InputValidator(); @@ -238,6 +255,7 @@ await Parallel.ForEachAsync( { try { + var formatStopwatch = Stopwatch.StartNew(); var normalizedFormat = format.ToLowerInvariant(); switch (normalizedFormat) @@ -246,49 +264,49 @@ await Parallel.ForEachAsync( var jsonExporter = new JsonExporter(); var jsonPath = Path.Combine(outputDir, "graph.json"); await jsonExporter.ExportAsync(graph, jsonPath, cancellationToken); - await WriteLineAsync($" Exported JSON: {jsonPath}"); + await WriteLineAsync($" Exported JSON: {jsonPath}{FormatWithElapsed(formatStopwatch.Elapsed)}"); break; case "html": var htmlExporter = new HtmlExporter(); var htmlPath = Path.Combine(outputDir, "graph.html"); await htmlExporter.ExportAsync(graph, htmlPath, communityLabels, cancellationToken); - await WriteLineAsync($" Exported HTML: {htmlPath}"); + await WriteLineAsync($" Exported HTML: {htmlPath}{FormatWithElapsed(formatStopwatch.Elapsed)}"); break; case "svg": var svgExporter = new SvgExporter(); var svgPath = Path.Combine(outputDir, "graph.svg"); await svgExporter.ExportAsync(graph, svgPath, cancellationToken); - await WriteLineAsync($" Exported SVG: {svgPath}"); + await WriteLineAsync($" Exported SVG: {svgPath}{FormatWithElapsed(formatStopwatch.Elapsed)}"); break; case "neo4j": var neo4jExporter = new Neo4jExporter(); var cypherPath = Path.Combine(outputDir, "graph.cypher"); await neo4jExporter.ExportAsync(graph, cypherPath, cancellationToken); - await WriteLineAsync($" Exported Neo4j Cypher: {cypherPath}"); + await WriteLineAsync($" Exported Neo4j Cypher: {cypherPath}{FormatWithElapsed(formatStopwatch.Elapsed)}"); break; case "ladybug": var ladybugExporter = new LadybugExporter(); var ladybugPath = Path.Combine(outputDir, "graph.ladybug.cypher"); await ladybugExporter.ExportAsync(graph, ladybugPath, cancellationToken); - await WriteLineAsync($" Exported Ladybug Cypher: {ladybugPath}"); + await WriteLineAsync($" Exported Ladybug Cypher: {ladybugPath}{FormatWithElapsed(formatStopwatch.Elapsed)}"); break; case "obsidian": var obsidianExporter = new ObsidianExporter(); var obsidianPath = Path.Combine(outputDir, "obsidian"); await obsidianExporter.ExportAsync(graph, obsidianPath, cancellationToken); - await WriteLineAsync($" Exported Obsidian vault: {obsidianPath}/"); + await WriteLineAsync($" Exported Obsidian vault: {obsidianPath}/{FormatWithElapsed(formatStopwatch.Elapsed)}"); break; case "wiki": var wikiExporter = new WikiExporter(); var wikiPath = Path.Combine(outputDir, "wiki"); await wikiExporter.ExportAsync(graph, wikiPath, cancellationToken); - await WriteLineAsync($" Exported Wiki: {wikiPath}/"); + await WriteLineAsync($" Exported Wiki: {wikiPath}/{FormatWithElapsed(formatStopwatch.Elapsed)}"); break; case "report": @@ -297,7 +315,7 @@ await Parallel.ForEachAsync( var reportMarkdown = reportGenerator.Generate(graph, analysis, communityLabels, cohesionScores, projectName); var reportPath = Path.Combine(outputDir, "GRAPH_REPORT.md"); await File.WriteAllTextAsync(reportPath, reportMarkdown, cancellationToken); - await WriteLineAsync($" Exported Report: {reportPath}"); + await WriteLineAsync($" Exported Report: {reportPath}{FormatWithElapsed(formatStopwatch.Elapsed)}"); break; default: @@ -310,9 +328,11 @@ await Parallel.ForEachAsync( await WriteLineAsync($" Error exporting {format}: {ex.Message}"); } } + await WriteLineAsync($" Export stage completed in {FormatElapsed(exportStopwatch.Elapsed)}"); await WriteLineAsync(); await WriteLineAsync("✓ Pipeline completed successfully"); + await WriteLineAsync($"Total runtime: {FormatElapsed(pipelineStopwatch.Elapsed)}"); await WriteLineAsync(); // Print summary @@ -371,6 +391,18 @@ private static bool ShouldReportProgress(int completed, int total, int step) return completed == total || completed % step == 0; } + private static string FormatElapsed(TimeSpan elapsed) + { + return elapsed.TotalSeconds >= 1 + ? $"{elapsed.TotalSeconds:F2}s" + : $"{elapsed.TotalMilliseconds:F0}ms"; + } + + private string FormatWithElapsed(TimeSpan elapsed) + { + return _verbose ? $" ({FormatElapsed(elapsed)})" : string.Empty; + } + private static Dictionary BuildCommunityLabels(KnowledgeGraph graph) { var result = new Dictionary(); diff --git a/src/Graphify/Pipeline/WatchMode.cs b/src/Graphify/Pipeline/WatchMode.cs index 2688de7..ba05de0 100644 --- a/src/Graphify/Pipeline/WatchMode.cs +++ b/src/Graphify/Pipeline/WatchMode.cs @@ -1,4 +1,5 @@ using System.Collections.Concurrent; +using System.Diagnostics; using Graphify.Cache; using Graphify.Export; using Graphify.Graph; @@ -127,6 +128,7 @@ private async Task ProcessChangesAsync( try { + var updateStopwatch = Stopwatch.StartNew(); // Filter to files that actually changed content (via SHA256) var trulyChanged = new List(); foreach (var filePath in changedPaths) @@ -148,6 +150,11 @@ private async Task ProcessChangesAsync( } } + if (_verbose) + { + await _output.WriteLineAsync($" Scanned {changedPaths.Count} path(s) in {FormatElapsed(updateStopwatch.Elapsed)}"); + } + if (trulyChanged.Count == 0) { if (_verbose) @@ -173,7 +180,12 @@ private async Task ProcessChangesAsync( MaxFileSizeBytes: 1024 * 1024, RespectGitIgnore: true); + var detectStopwatch = Stopwatch.StartNew(); var allDetected = await fileDetector.ExecuteAsync(options, ct); + if (_verbose) + { + await _output.WriteLineAsync($" Re-detected {allDetected.Count} file(s) in {FormatElapsed(detectStopwatch.Elapsed)}"); + } // Filter to changed files only var changedSet = new HashSet(trulyChanged, StringComparer.OrdinalIgnoreCase); @@ -190,6 +202,8 @@ private async Task ProcessChangesAsync( var newResults = new List(); var progressStep = Math.Max(1, filesToProcess.Count / 10); var completed = 0; + var extractStopwatch = Stopwatch.StartNew(); + var extracted = 0; foreach (var file in filesToProcess) { ct.ThrowIfCancellationRequested(); @@ -197,7 +211,10 @@ private async Task ProcessChangesAsync( { var result = await extractor.ExecuteAsync(file, ct); if (result.Nodes.Count > 0 || result.Edges.Count > 0) + { newResults.Add(result); + extracted++; + } } catch (Exception ex) { @@ -206,9 +223,10 @@ private async Task ProcessChangesAsync( } completed++; - if (completed == filesToProcess.Count || completed % progressStep == 0) + if (ShouldReportProgress(completed, filesToProcess.Count, progressStep)) { - await _output.WriteLineAsync($" Progress: {completed}/{filesToProcess.Count} changed files analyzed"); + await _output.WriteLineAsync( + $" Progress: {completed}/{filesToProcess.Count} changed files analyzed ({extracted} produced output, {FormatElapsed(extractStopwatch.Elapsed)})"); } } @@ -226,10 +244,16 @@ private async Task ProcessChangesAsync( MergeStrategy = MergeStrategy.MostRecent }); + var buildStopwatch = Stopwatch.StartNew(); var incrementalGraph = await graphBuilder.ExecuteAsync(newResults, ct); _currentGraph!.MergeGraph(incrementalGraph); + if (_verbose) + { + await _output.WriteLineAsync($" Graph merge completed in {FormatElapsed(buildStopwatch.Elapsed)}"); + } // Re-cluster + var clusterStopwatch = Stopwatch.StartNew(); var clusterEngine = new ClusterEngine(new ClusterOptions { MaxIterations = 100, @@ -238,8 +262,13 @@ private async Task ProcessChangesAsync( MaxCommunityFraction = 0.2 }); _currentGraph = await clusterEngine.ExecuteAsync(_currentGraph, ct); + if (_verbose) + { + await _output.WriteLineAsync($" Re-clustering completed in {FormatElapsed(clusterStopwatch.Elapsed)}"); + } // Re-export + var exportStopwatch = Stopwatch.StartNew(); Directory.CreateDirectory(outputDir); foreach (var format in formats) { @@ -254,9 +283,14 @@ private async Task ProcessChangesAsync( break; } } + if (_verbose) + { + await _output.WriteLineAsync($" Export completed in {FormatElapsed(exportStopwatch.Elapsed)}"); + } await _output.WriteLineAsync($" Re-processed {newResults.Count} file(s) → {_currentGraph.NodeCount} nodes, {_currentGraph.EdgeCount} edges"); await _output.WriteLineAsync($" Exported to {outputDir}"); + await _output.WriteLineAsync($" Incremental update total time: {FormatElapsed(updateStopwatch.Elapsed)}"); await _output.WriteLineAsync(); } catch (OperationCanceledException) @@ -281,4 +315,16 @@ public void Dispose() _watcher.Dispose(); _processLock.Dispose(); } + + private static string FormatElapsed(TimeSpan elapsed) + { + return elapsed.TotalSeconds >= 1 + ? $"{elapsed.TotalSeconds:F2}s" + : $"{elapsed.TotalMilliseconds:F0}ms"; + } + + private static bool ShouldReportProgress(int completed, int total, int step) + { + return completed == total || completed % step == 0; + } }