From 4fb8883467f82a0374c9550372bc7d3088ffbeeb Mon Sep 17 00:00:00 2001 From: barry Date: Thu, 19 Mar 2026 15:13:54 +0800 Subject: [PATCH 01/26] feat: add runtime statistics tracking for providers - Introduced ProviderRuntimeStats struct to capture metrics such as call count, total duration, average duration, last duration, last error, and last run timestamp. - Enhanced GetProviderDetails to include function file and line number for better debugging. - Implemented GetProviderRuntimeStats method to return sorted runtime statistics for all providers. - Added recordProviderStat method to track provider execution metrics. - Updated the Dix struct to maintain provider statistics. - Modified error handling to optionally print stack traces based on log level. - Added tests for runtime statistics retrieval and validation of provider initialization. - Created new server runtime test file to validate HTTP endpoint for runtime stats. --- dixhttp/README.md | 54 ++- dixhttp/server.go | 24 ++ dixhttp/server_runtime_test.go | 76 +++++ dixhttp/template.html | 597 ++++++++++++++++++++------------- dixinternal/api.go | 89 +++++ dixinternal/dix.go | 76 ++++- dixinternal/dix_test.go | 78 +++++ dixinternal/logger.go | 15 + dixinternal/provider.go | 3 +- 9 files changed, 748 insertions(+), 264 deletions(-) create mode 100644 dixhttp/server_runtime_test.go diff --git a/dixhttp/README.md b/dixhttp/README.md index 3e679f7..9847dd2 100644 --- a/dixhttp/README.md +++ b/dixhttp/README.md @@ -15,6 +15,7 @@ This module provides an HTTP server to visualize dependency relationships in the - 🧩 **Group Rules (Prefix Aggregation)** - Aggregate nodes by package/prefix rules - 🔎 **Prefix Filter** - Show only nodes/providers matching a prefix - 🧭 **Group Subgraph** - View a group's internal + upstream/downstream dependencies +- ⏱️ **Startup Runtime Stats** - Show all providers' startup durations (total/avg/last, call count), with executed-only filter (`call_count > 0`) - 📡 **RESTful API** - Provide JSON format dependency data - 🧩 **Mermaid Export/Preview** - Generate Mermaid flowcharts for current graph (respects grouping/filtering) @@ -97,11 +98,11 @@ server := dixhttp.NewServerWithOptions( ### Three-Panel Layout -| Area | Description | -|------|-------------| -| **Left - Package List** | Provider list grouped by package, searchable, collapsible | -| **Center - Dependency Graph** | Interactive graph with drag, zoom, click support | -| **Right - Details Panel** | Show selected node details with clickable navigation | +| Area | Description | +| ----------------------------- | --------------------------------------------------------- | +| **Left - Package List** | Provider list grouped by package, searchable, collapsible | +| **Center - Dependency Graph** | Interactive graph with drag, zoom, click support | +| **Right - Details Panel** | Show selected node details with clickable navigation | ## Core Features @@ -146,12 +147,12 @@ After searching or clicking a type, the system shows that type as center: Depth determines how many levels to expand up/down: -| Depth | Description | Use Case | -|-------|-------------|----------| -| 1 | Only direct dependencies/dependents | Quick view of direct relationships | -| 2 | Two levels (default) | Recommended for daily use | -| 3-5 | More levels | Track complex dependency chains | -| All | Show complete dependency tree | Small projects or specific analysis | +| Depth | Description | Use Case | +| ----- | ----------------------------------- | ----------------------------------- | +| 1 | Only direct dependencies/dependents | Quick view of direct relationships | +| 2 | Two levels (default) | Recommended for daily use | +| 3-5 | More levels | Track complex dependency chains | +| All | Show complete dependency tree | Small projects or specific analysis | **Example**: Assume dependency chain is `Config → Database → UserService → Handler` @@ -215,13 +216,13 @@ Left panel features: ## Interactions -| Operation | Effect | -|-----------|--------| -| **Single Click** | Show details in right panel | -| **Double Click** | Show dependency graph centered on that node | -| **Drag Node** | Move node position | -| **Scroll Zoom** | Zoom in/out graph | -| **Click Type in Details** | Jump to view that type's dependencies | +| Operation | Effect | +| ------------------------- | ------------------------------------------- | +| **Single Click** | Show details in right panel | +| **Double Click** | Show dependency graph centered on that node | +| **Drag Node** | Move node position | +| **Scroll Zoom** | Zoom in/out graph | +| **Click Type in Details** | Jump to view that type's dependencies | ## Mermaid Support @@ -249,6 +250,23 @@ Returns summary statistics } ``` +### GET `/api/runtime-stats?limit=20` +Returns provider runtime metrics sorted by total duration (desc), useful for finding slow startup components. + +```json +[ + { + "function_name": "main.NewUserService", + "output_type": "*service.UserService", + "call_count": 1, + "total_duration": 3456789, + "average_duration": 3456789, + "last_duration": 3456789, + "last_run_at_unix_nano": 1700000000000000000 + } +] +``` + ### GET `/api/packages` Returns package list diff --git a/dixhttp/server.go b/dixhttp/server.go index eeaf73a..47cf2e1 100644 --- a/dixhttp/server.go +++ b/dixhttp/server.go @@ -129,6 +129,7 @@ func (s *Server) setupRoutes() { s.mux.HandleFunc(indexPath, s.HandleIndex) s.mux.HandleFunc(base+"/api/dependencies", s.HandleDependencies) s.mux.HandleFunc(base+"/api/stats", s.HandleStats) + s.mux.HandleFunc(base+"/api/runtime-stats", s.HandleRuntimeStats) s.mux.HandleFunc(base+"/api/packages", s.HandlePackages) s.mux.HandleFunc(base+"/api/package/", s.HandlePackageDetails) s.mux.HandleFunc(base+"/api/type/", s.HandleTypeDetails) @@ -191,6 +192,25 @@ func (s *Server) HandleStats(w http.ResponseWriter, r *http.Request) { writeJSON(w, stats) } +// HandleRuntimeStats returns provider runtime stats for startup/perf diagnosis. +// Query params: +// - limit: optional positive integer to limit returned rows. +func (s *Server) HandleRuntimeStats(w http.ResponseWriter, r *http.Request) { + limit := 0 + if limitStr := r.URL.Query().Get("limit"); limitStr != "" { + if l, err := strconv.Atoi(limitStr); err == nil && l > 0 { + limit = l + } + } + + stats := s.dix.GetProviderRuntimeStats() + if limit > 0 && len(stats) > limit { + stats = stats[:limit] + } + + writeJSON(w, stats) +} + // HandlePackages returns list of packages for navigation func (s *Server) HandlePackages(w http.ResponseWriter, r *http.Request) { providerDetails := s.dix.GetProviderDetails() @@ -437,6 +457,8 @@ func (s *Server) extractDependencyData(pkgFilter string, limit int) *DependencyD OutputPkg: detail.OutputPkg, FunctionName: detail.FunctionName, FunctionPkg: detail.FunctionPkg, + FunctionFile: detail.FunctionFile, + FunctionLine: detail.FunctionLine, InputTypes: detail.InputTypes, InputPkgs: detail.InputPkgs, } @@ -545,6 +567,8 @@ type ProviderInfo struct { OutputPkg string `json:"output_pkg"` FunctionName string `json:"function_name"` FunctionPkg string `json:"function_pkg"` + FunctionFile string `json:"function_file"` + FunctionLine int `json:"function_line"` InputTypes []string `json:"input_types"` InputPkgs []string `json:"input_pkgs"` } diff --git a/dixhttp/server_runtime_test.go b/dixhttp/server_runtime_test.go new file mode 100644 index 0000000..7a29109 --- /dev/null +++ b/dixhttp/server_runtime_test.go @@ -0,0 +1,76 @@ +package dixhttp + +import ( + "encoding/json" + "net/http" + "net/http/httptest" + "testing" + + "github.com/pubgo/dix/v2/dixinternal" +) + +type runtimeStatsDep struct{} + +func TestHandleRuntimeStats(t *testing.T) { + di := dixinternal.New() + di.Provide(func() *runtimeStatsDep { return &runtimeStatsDep{} }) + + if err := di.TryInject(func(*runtimeStatsDep) {}); err != nil { + t.Fatalf("failed to initialize dependency: %v", err) + } + + server := NewServer(di) + req := httptest.NewRequest(http.MethodGet, "/api/runtime-stats?limit=1", nil) + rr := httptest.NewRecorder() + + server.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code) + } + + var stats []dixinternal.ProviderRuntimeStats + if err := json.Unmarshal(rr.Body.Bytes(), &stats); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if len(stats) == 0 { + t.Fatal("expected at least one runtime stat record") + } + + if len(stats) > 1 { + t.Fatalf("expected response limited to 1 item, got %d", len(stats)) + } +} + +func TestHandleRuntimeStatsIncludeUninitializedProviders(t *testing.T) { + type depA struct{} + type depB struct{} + + di := dixinternal.New() + di.Provide(func() *depA { return &depA{} }) + di.Provide(func() *depB { return &depB{} }) + + if err := di.TryInject(func(*depA) {}); err != nil { + t.Fatalf("failed to initialize depA: %v", err) + } + + server := NewServer(di) + req := httptest.NewRequest(http.MethodGet, "/api/runtime-stats", nil) + rr := httptest.NewRecorder() + + server.ServeHTTP(rr, req) + + if rr.Code != http.StatusOK { + t.Fatalf("expected status %d, got %d", http.StatusOK, rr.Code) + } + + var stats []dixinternal.ProviderRuntimeStats + if err := json.Unmarshal(rr.Body.Bytes(), &stats); err != nil { + t.Fatalf("failed to decode response: %v", err) + } + + if len(stats) < 2 { + t.Fatalf("expected at least 2 providers in runtime stats, got %d", len(stats)) + } +} diff --git a/dixhttp/template.html b/dixhttp/template.html index 6a43b10..0eeff31 100644 --- a/dixhttp/template.html +++ b/dixhttp/template.html @@ -1,5 +1,6 @@ + @@ -9,7 +10,10 @@