ctxman 3.0.0 → 3.1.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.ctxman/llm-profiles.json +1067 -39
- package/.ctxman/profiles/backend-team.json +13 -0
- package/.ctxman/profiles/default.json +12 -0
- package/.ctxman/profiles/devops-team.json +21 -0
- package/.ctxman/profiles/frontend-team.json +20 -0
- package/.ctxman/templates/bug-fix.json +17 -0
- package/.ctxman/templates/code-review.json +9 -0
- package/.ctxman/templates/documentation.json +9 -0
- package/.ctxman/templates/feature.json +9 -0
- package/.ctxman/templates/full-context.json +9 -0
- package/.ctxman/templates/refactoring.json +18 -0
- package/.ctxman/templates/testing.json +9 -0
- package/.ctxman/wizard-profiles/security-audit/profile.json +7 -1
- package/LICENSE +21 -0
- package/README.md +381 -136
- package/bin/cli.js +1880 -529
- package/bin/cm-ask.js +18 -67
- package/bin/cm-audit.js +121 -0
- package/bin/cm-count.js +151 -0
- package/bin/cm-gitingest.js +128 -115
- package/bin/cm-map.js +242 -0
- package/bin/cm-mcp.js +293 -0
- package/bin/cm-pack.js +405 -0
- package/bin/cm-policy.js +170 -0
- package/bin/cm-pr.js +248 -0
- package/bin/cm-search.js +178 -0
- package/bin/cm-update.js +128 -128
- package/bin/cm-why.js +197 -0
- package/bin/mcp-server.js +8 -24
- package/ctxman.js +237 -183
- package/examples/README.md +19 -4
- package/examples/github-actions/ctxman-check.yml +49 -0
- package/examples/wizard-profiles/security-audit/profile.json +7 -1
- package/index.d.ts +1422 -0
- package/index.js +55 -31
- package/lib/agents/pr-failure-agent.js +48 -45
- package/lib/analyzers/ai-suggester.js +451 -0
- package/lib/analyzers/dependency-scanner.js +505 -0
- package/lib/analyzers/duplicate-detector.js +264 -0
- package/lib/analyzers/go-method-analyzer.js +151 -110
- package/lib/analyzers/import-tracker.js +431 -0
- package/lib/analyzers/method-analyzer.js +1055 -434
- package/lib/analyzers/performance-dashboard.js +687 -0
- package/lib/analyzers/token-calculator.js +895 -504
- package/lib/analyzers/unused-exports-detector.js +336 -0
- package/lib/api/mcp/install.js +424 -0
- package/lib/api/mcp/prompts.js +281 -281
- package/lib/api/mcp/resources.js +300 -241
- package/lib/api/mcp/sandbox.js +89 -0
- package/lib/api/mcp/server.js +772 -312
- package/lib/api/mcp/start.js +125 -0
- package/lib/api/mcp/transports/HttpSseTransport.js +60 -52
- package/lib/api/rest/server.js +105 -28
- package/lib/cache/CacheManager.js +9 -17
- package/lib/core/Analyzer.js +16 -10
- package/lib/core/ContextBuilder.js +19 -28
- package/lib/core/Reporter.js +15 -14
- package/lib/core/Scanner.js +81 -12
- package/lib/count/index.js +100 -0
- package/lib/formatters/format-registry.js +394 -372
- package/lib/formatters/gitingest-formatter.js +656 -621
- package/lib/formatters/toon-formatter-v1.3.js +637 -632
- package/lib/formatters/toon-formatter.js +239 -240
- package/lib/integrations/git/BlameTracker.js +4 -4
- package/lib/integrations/git/DiffAnalyzer.js +38 -31
- package/lib/integrations/git/GitClient.js +71 -53
- package/lib/lsp/server.js +311 -0
- package/lib/map/graph.js +299 -0
- package/lib/map/imports.js +233 -0
- package/lib/map/index.js +33 -0
- package/lib/map/rank.js +253 -0
- package/lib/map/render.js +346 -0
- package/lib/map/resolver.js +574 -0
- package/lib/pack/budget.js +111 -0
- package/lib/pack/code-lexer.js +360 -0
- package/lib/pack/compress.js +414 -0
- package/lib/pack/index.js +354 -0
- package/lib/pack/renderers/common.js +134 -0
- package/lib/pack/renderers/index.js +24 -0
- package/lib/pack/renderers/json.js +47 -0
- package/lib/pack/renderers/markdown.js +54 -0
- package/lib/pack/renderers/plain.js +38 -0
- package/lib/pack/renderers/xml.js +85 -0
- package/lib/pack/select.js +177 -0
- package/lib/pack/split.js +182 -0
- package/lib/pack/tree.js +35 -0
- package/lib/parsers/gitignore-parser.js +116 -107
- package/lib/parsers/method-filter-parser.js +50 -33
- package/lib/plugins/ExporterPlugin.js +3 -3
- package/lib/plugins/LanguagePlugin.js +3 -3
- package/lib/plugins/PluginManager.js +7 -13
- package/lib/policy/audit.js +312 -0
- package/lib/policy/enforce.js +125 -0
- package/lib/policy/ignore-files.js +169 -0
- package/lib/policy/index.js +53 -0
- package/lib/policy/policy.js +489 -0
- package/lib/pr/diff-parser.js +267 -0
- package/lib/pr/method-mapper.js +148 -0
- package/lib/pr/pr-context.js +829 -0
- package/lib/pr/pr-git.js +149 -0
- package/lib/pr/pr-renderer.js +644 -0
- package/lib/pr/related-tests.js +296 -0
- package/lib/rag/DataSourcePlugin.js +5 -5
- package/lib/rag/EmbeddingProvider.js +19 -8
- package/lib/rag/Indexer.js +14 -5
- package/lib/rag/VectorStoreFactory.js +3 -3
- package/lib/rag/ask.js +122 -0
- package/lib/rag/sources/FileDataSource.js +12 -9
- package/lib/rag/stores/BaseVectorStore.js +5 -5
- package/lib/rag/stores/ChromaDBStore.js +10 -10
- package/lib/rag/stores/LanceDBStore.js +29 -19
- package/lib/rag/stores/PineconeStore.js +11 -9
- package/lib/search/bm25.js +433 -0
- package/lib/search/code-units.js +201 -0
- package/lib/search/symbols.js +189 -0
- package/lib/ui/dashboard.js +120 -89
- package/lib/ui/index.js +1 -6
- package/lib/ui/init-wizard-ui.js +550 -0
- package/lib/ui/progress-bar.js +94 -88
- package/lib/ui/select-input.js +40 -32
- package/lib/ui/template-selector.js +99 -0
- package/lib/ui/wizard.js +374 -329
- package/lib/utils/claude-token-counter.js +151 -0
- package/lib/utils/cli-output.js +17 -0
- package/lib/utils/clipboard-utils.js +49 -50
- package/lib/utils/config-utils.js +81 -66
- package/lib/utils/context-versioning.js +532 -0
- package/lib/utils/error-handler.js +112 -112
- package/lib/utils/file-utils.js +153 -69
- package/lib/utils/format-converter.js +356 -283
- package/lib/utils/git-utils.js +366 -294
- package/lib/utils/llm-cost-estimator.js +281 -0
- package/lib/utils/llm-detector.js +242 -78
- package/lib/utils/logger.js +246 -237
- package/lib/utils/messages.js +355 -0
- package/lib/utils/multi-repo-manager.js +364 -0
- package/lib/utils/profile-manager.js +380 -0
- package/lib/utils/safe-read.js +61 -0
- package/lib/utils/secret-redactor.js +61 -0
- package/lib/utils/snapshot-manager.js +679 -0
- package/lib/utils/template-manager.js +378 -0
- package/lib/utils/token-utils.js +201 -170
- package/lib/utils/tokenizer-adapter.js +421 -377
- package/lib/utils/updater.js +539 -506
- package/lib/watch/FileWatcher.js +12 -8
- package/lib/watch/IncrementalAnalyzer.js +7 -7
- package/lib/why/coupling.js +81 -0
- package/lib/why/errors.js +23 -0
- package/lib/why/git.js +169 -0
- package/lib/why/history.js +108 -0
- package/lib/why/index.js +720 -0
- package/lib/why/ownership.js +79 -0
- package/lib/why/paths.js +207 -0
- package/lib/why/references.js +259 -0
- package/lib/why/render.js +722 -0
- package/lib/why/search.js +358 -0
- package/lib/why/target.js +389 -0
- package/lib/wizards/config-generator.js +257 -0
- package/lib/wizards/project-detector.js +572 -0
- package/package.json +44 -10
- package/.ctxman/logs/ctxman-2026-07-02.log +0 -156
- package/.ctxman/self-knowledge/documents.lance/_transactions/0-b3dd1558-f95b-44b9-a154-18e8c330ae7a.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/1-44bff6d4-e552-4010-a769-e09ad822698d.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/10-37260794-5947-4ecb-b4c8-d732f58f40f1.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/11-0d52a883-5518-463c-9b6d-986c3af89dfd.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/12-ae354646-52f2-49c9-90fb-2e6ed8932663.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/13-5d7d5e57-27da-498b-b734-091b9d38da13.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/14-558ce928-810b-4252-a364-fdd1ee6f8a25.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/15-27e3a58f-8b64-43b8-a9f4-e3369446968b.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/16-a110c390-e5c8-4464-b976-bc7f5195fa15.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/17-97513225-53de-4216-b61a-6337bd0cc4ad.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/18-5ba450f0-c527-459c-bf4f-6d79b5e4285f.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/19-600d3a63-5225-49e9-b8b7-30e037b564ab.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/2-48a61b4e-92cf-4342-8376-914c1b99a9f9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/20-121dd912-5410-410e-84ff-82c7a23604a0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/3-bc6844ab-7ea3-4bb3-9ee1-538ce5664a9c.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/4-d5e1754f-dbb5-454e-a9be-2a7da8d7bcf9.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/5-4fae93df-0568-4b6f-8acf-8bef4bba6047.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/6-4b1b8b87-068e-49a3-ba2b-d8dae628b252.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/7-63d040fb-d2fa-4c0f-bed0-53411918a7b0.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/8-c5628cf6-264c-420f-aab8-bc96a9083591.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_transactions/9-c94570aa-5e53-4fcb-971f-cecc1d1d5492.txn +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/1.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/10.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/11.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/12.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/13.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/14.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/15.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/16.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/17.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/18.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/19.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/2.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/20.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/21.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/3.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/4.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/5.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/6.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/7.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/8.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/_versions/9.manifest +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000010010011011101000010de8621405d9d5cb06540dbf02c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/0000101001011100011000003fb62a4aa8978974075c144f1c.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/000011010000100111001011c6cc824d628f3bb1f4e04cd5d0.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/001000001000011001011100d8f74349ada090b332cd1df669.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000001111010001000010045ab44467bb1e7e4d8ae06bd58.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/01000101101110111110011131ef904241a89e1ffb6ddaf8e3.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/010010110000010000011010fbfcc145c6a8a827b28057a07e.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1000011000101010001110003fb4f045c3835982efa1405324.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111011101110111101011f4396b410fa06af3b028d204e6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/100111100111101000001100596b174e00995d6cc2969cc8dc.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1010001100011000110010102ea16b484d887c7c9725feffd6.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/101101100010101111110111c5230944d9b3af7301b6392dda.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1011011100100111111110105892bf41b5a530fedd7cf516f1.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/10111110000010001011110164e8774742bade87b483c10f36.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1100000100010110001110103ffaca40a2a42c85067ebaf9ae.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/110000010001110110101110664bde4c76a78dfc14b05e83b8.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11001100111000010100110050645c4281b1f35b2022234f7f.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1101111011001100110111017214924a368f5719eab2246619.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110000111001010001000002150c24dcab653d39d07032e63.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/1110100000000100000011014d20534967a18b5c87721659ff.lance +0 -0
- package/.ctxman/self-knowledge/documents.lance/data/11110101111010000101011135a0114ef2b35dc6f1297a3381.lance +0 -0
|
@@ -0,0 +1,687 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Dashboard - Text-based ASCII charts for terminal output (FEAT-001)
|
|
3
|
+
*
|
|
4
|
+
* A lightweight, text-based performance dashboard that displays metrics
|
|
5
|
+
* with ASCII visualization without requiring Ink/React dependencies.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
10
|
+
import { TokenAnalyzer } from '../../index.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* PerformanceDashboard class
|
|
14
|
+
* Generates text-based performance metrics with ASCII charts
|
|
15
|
+
*/
|
|
16
|
+
export class PerformanceDashboard {
|
|
17
|
+
constructor(options = {}) {
|
|
18
|
+
this.projectRoot = options.projectRoot || process.cwd();
|
|
19
|
+
this.storagePath = options.storagePath || path.join(this.projectRoot, '.ctxman', 'metrics');
|
|
20
|
+
this.metrics = [];
|
|
21
|
+
this.currentMetrics = null;
|
|
22
|
+
this.startTime = null;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Start tracking a new analysis session
|
|
27
|
+
*/
|
|
28
|
+
startAnalysis() {
|
|
29
|
+
this.startTime = Date.now();
|
|
30
|
+
this.currentMetrics = {
|
|
31
|
+
id: this.generateId(),
|
|
32
|
+
startedAt: new Date().toISOString(),
|
|
33
|
+
phases: {},
|
|
34
|
+
result: null,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Record a phase during analysis
|
|
40
|
+
* @param {string} name - Phase name (e.g., 'discovery', 'analysis', 'output')
|
|
41
|
+
* @param {number} duration - Duration in milliseconds
|
|
42
|
+
* @param {object} metadata - Additional metadata
|
|
43
|
+
*/
|
|
44
|
+
recordPhase(name, duration, metadata = {}) {
|
|
45
|
+
if (!this.currentMetrics) {
|
|
46
|
+
this.currentMetrics = {
|
|
47
|
+
id: this.generateId(),
|
|
48
|
+
startedAt: new Date().toISOString(),
|
|
49
|
+
phases: {},
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
this.currentMetrics.phases[name] = {
|
|
53
|
+
duration,
|
|
54
|
+
...metadata,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* End the current analysis session
|
|
60
|
+
* @param {object} result - Analysis result with totalFiles, totalTokens, cacheHits, cacheMisses
|
|
61
|
+
*/
|
|
62
|
+
endAnalysis(result) {
|
|
63
|
+
if (!this.currentMetrics) return;
|
|
64
|
+
|
|
65
|
+
const endTime = Date.now();
|
|
66
|
+
this.currentMetrics.completedAt = new Date().toISOString();
|
|
67
|
+
this.currentMetrics.totalDuration = endTime - this.startTime;
|
|
68
|
+
this.currentMetrics.result = {
|
|
69
|
+
totalFiles: result.totalFiles || 0,
|
|
70
|
+
totalTokens: result.totalTokens || 0,
|
|
71
|
+
cacheHits: result.cacheHits || 0,
|
|
72
|
+
cacheMisses: result.cacheMisses || 0,
|
|
73
|
+
totalBytes: result.totalBytes || 0,
|
|
74
|
+
totalLines: result.totalLines || 0,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
this.metrics.push(this.currentMetrics);
|
|
78
|
+
this.persist();
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Generate unique ID for metrics entry
|
|
83
|
+
*/
|
|
84
|
+
generateId() {
|
|
85
|
+
return `perf-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Persist metrics to storage
|
|
90
|
+
*/
|
|
91
|
+
persist() {
|
|
92
|
+
try {
|
|
93
|
+
// Ensure directory exists
|
|
94
|
+
if (!fs.existsSync(this.storagePath)) {
|
|
95
|
+
fs.mkdirSync(this.storagePath, { recursive: true });
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const metricsFile = path.join(this.storagePath, 'metrics.json');
|
|
99
|
+
let data = [];
|
|
100
|
+
try {
|
|
101
|
+
data = JSON.parse(fs.readFileSync(metricsFile, 'utf8'));
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// No metrics yet; an unreadable file stops the save, as before
|
|
104
|
+
if (error.code !== 'ENOENT') throw error;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
data.push(...this.metrics);
|
|
108
|
+
fs.writeFileSync(metricsFile, JSON.stringify(data, null, 2));
|
|
109
|
+
|
|
110
|
+
// Clear in-memory metrics after persisting
|
|
111
|
+
this.metrics = [];
|
|
112
|
+
} catch (_error) {
|
|
113
|
+
// Silently fail - metrics are best-effort
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Load historical metrics from storage
|
|
119
|
+
*/
|
|
120
|
+
loadMetrics() {
|
|
121
|
+
try {
|
|
122
|
+
const metricsFile = path.join(this.storagePath, 'metrics.json');
|
|
123
|
+
if (fs.existsSync(metricsFile)) {
|
|
124
|
+
return JSON.parse(fs.readFileSync(metricsFile, 'utf8'));
|
|
125
|
+
}
|
|
126
|
+
} catch (_error) {
|
|
127
|
+
// Return empty array if loading fails
|
|
128
|
+
}
|
|
129
|
+
return [];
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Get aggregated metrics for a time period
|
|
134
|
+
* @param {string} period - Period string ('7d', '30d', 'all')
|
|
135
|
+
*/
|
|
136
|
+
getAggregatedMetrics(period = '7d') {
|
|
137
|
+
const historicalMetrics = this.loadMetrics();
|
|
138
|
+
const allMetrics = [...historicalMetrics, ...this.metrics];
|
|
139
|
+
|
|
140
|
+
if (allMetrics.length === 0) {
|
|
141
|
+
return this.getEmptyMetrics();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const cutoff = this.getCutoffDate(period);
|
|
145
|
+
const filtered = allMetrics.filter((m) => new Date(m.startedAt) >= cutoff);
|
|
146
|
+
|
|
147
|
+
if (filtered.length === 0) {
|
|
148
|
+
return this.getEmptyMetrics();
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
return {
|
|
152
|
+
totalAnalyses: filtered.length,
|
|
153
|
+
avgDuration: this.average(filtered.map((m) => m.totalDuration || 0)),
|
|
154
|
+
avgFiles: this.average(filtered.map((m) => m.result?.totalFiles || 0)),
|
|
155
|
+
avgTokens: this.average(filtered.map((m) => m.result?.totalTokens || 0)),
|
|
156
|
+
cacheHitRate: this.calculateCacheHitRate(filtered),
|
|
157
|
+
totalFiles: filtered.reduce((sum, m) => sum + (m.result?.totalFiles || 0), 0),
|
|
158
|
+
totalTokens: filtered.reduce((sum, m) => sum + (m.result?.totalTokens || 0), 0),
|
|
159
|
+
phaseBreakdown: this.aggregatePhases(filtered),
|
|
160
|
+
trend: this.calculateTrend(filtered),
|
|
161
|
+
period,
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Get empty metrics structure
|
|
167
|
+
*/
|
|
168
|
+
getEmptyMetrics() {
|
|
169
|
+
return {
|
|
170
|
+
totalAnalyses: 0,
|
|
171
|
+
avgDuration: 0,
|
|
172
|
+
avgFiles: 0,
|
|
173
|
+
avgTokens: 0,
|
|
174
|
+
cacheHitRate: 0,
|
|
175
|
+
totalFiles: 0,
|
|
176
|
+
totalTokens: 0,
|
|
177
|
+
phaseBreakdown: {},
|
|
178
|
+
trend: [],
|
|
179
|
+
period: '7d',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Get cutoff date for period
|
|
185
|
+
* @param {string} period - Period string
|
|
186
|
+
*/
|
|
187
|
+
getCutoffDate(period) {
|
|
188
|
+
const now = new Date();
|
|
189
|
+
switch (period) {
|
|
190
|
+
case '7d':
|
|
191
|
+
return new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
192
|
+
case '30d':
|
|
193
|
+
return new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
194
|
+
case 'all':
|
|
195
|
+
default:
|
|
196
|
+
return new Date(0); // Beginning of time
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Calculate average of numbers
|
|
202
|
+
* @param {number[]} values
|
|
203
|
+
*/
|
|
204
|
+
average(values) {
|
|
205
|
+
if (values.length === 0) return 0;
|
|
206
|
+
const sum = values.reduce((a, b) => a + b, 0);
|
|
207
|
+
return Math.round(sum / values.length);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
/**
|
|
211
|
+
* Calculate cache hit rate
|
|
212
|
+
* @param {object[]} metrics
|
|
213
|
+
*/
|
|
214
|
+
calculateCacheHitRate(metrics) {
|
|
215
|
+
let totalHits = 0;
|
|
216
|
+
let totalRequests = 0;
|
|
217
|
+
|
|
218
|
+
for (const m of metrics) {
|
|
219
|
+
if (m.result) {
|
|
220
|
+
totalHits += m.result.cacheHits || 0;
|
|
221
|
+
totalRequests += (m.result.cacheHits || 0) + (m.result.cacheMisses || 0);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
if (totalRequests === 0) return 0;
|
|
226
|
+
return Math.round((totalHits / totalRequests) * 100);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Aggregate phase durations across metrics
|
|
231
|
+
* @param {object[]} metrics
|
|
232
|
+
*/
|
|
233
|
+
aggregatePhases(metrics) {
|
|
234
|
+
const phases = {};
|
|
235
|
+
|
|
236
|
+
for (const m of metrics) {
|
|
237
|
+
if (m.phases) {
|
|
238
|
+
for (const [name, data] of Object.entries(m.phases)) {
|
|
239
|
+
if (!phases[name]) {
|
|
240
|
+
phases[name] = { totalDuration: 0, count: 0 };
|
|
241
|
+
}
|
|
242
|
+
phases[name].totalDuration += data.duration || 0;
|
|
243
|
+
phases[name].count++;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// Calculate averages
|
|
249
|
+
for (const name of Object.keys(phases)) {
|
|
250
|
+
phases[name].avgDuration = Math.round(phases[name].totalDuration / phases[name].count);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return phases;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Calculate trend data for charts
|
|
258
|
+
* @param {object[]} metrics
|
|
259
|
+
*/
|
|
260
|
+
calculateTrend(metrics) {
|
|
261
|
+
// Sort by date
|
|
262
|
+
const sorted = [...metrics].sort((a, b) => new Date(a.startedAt) - new Date(b.startedAt));
|
|
263
|
+
|
|
264
|
+
// Group by day
|
|
265
|
+
const byDay = {};
|
|
266
|
+
for (const m of sorted) {
|
|
267
|
+
const day = m.startedAt.split('T')[0];
|
|
268
|
+
if (!byDay[day]) {
|
|
269
|
+
byDay[day] = { count: 0, totalDuration: 0, totalTokens: 0 };
|
|
270
|
+
}
|
|
271
|
+
byDay[day].count++;
|
|
272
|
+
byDay[day].totalDuration += m.totalDuration || 0;
|
|
273
|
+
byDay[day].totalTokens += m.result?.totalTokens || 0;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
// Convert to array
|
|
277
|
+
return Object.entries(byDay)
|
|
278
|
+
.map(([date, data]) => ({
|
|
279
|
+
date,
|
|
280
|
+
count: data.count,
|
|
281
|
+
avgDuration: Math.round(data.totalDuration / data.count),
|
|
282
|
+
totalTokens: data.totalTokens,
|
|
283
|
+
}))
|
|
284
|
+
.slice(-30); // Last 30 days
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
/**
|
|
288
|
+
* Identify performance bottlenecks
|
|
289
|
+
* @param {object} metrics - Aggregated metrics
|
|
290
|
+
*/
|
|
291
|
+
identifyBottlenecks(metrics) {
|
|
292
|
+
const bottlenecks = [];
|
|
293
|
+
const { phaseBreakdown, avgDuration, avgFiles } = metrics;
|
|
294
|
+
|
|
295
|
+
// Check phase durations
|
|
296
|
+
if (phaseBreakdown) {
|
|
297
|
+
for (const [name, data] of Object.entries(phaseBreakdown)) {
|
|
298
|
+
const percentage = avgDuration > 0 ? Math.round((data.avgDuration / avgDuration) * 100) : 0;
|
|
299
|
+
|
|
300
|
+
if (percentage > 40 && data.avgDuration > 100) {
|
|
301
|
+
bottlenecks.push({
|
|
302
|
+
type: 'phase',
|
|
303
|
+
name,
|
|
304
|
+
avgDuration: data.avgDuration,
|
|
305
|
+
percentage,
|
|
306
|
+
severity: percentage > 70 ? 'high' : 'medium',
|
|
307
|
+
});
|
|
308
|
+
}
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// Check for large file counts
|
|
313
|
+
if (avgFiles > 1000) {
|
|
314
|
+
bottlenecks.push({
|
|
315
|
+
type: 'file_count',
|
|
316
|
+
name: 'Large project size',
|
|
317
|
+
avgFiles,
|
|
318
|
+
severity: 'medium',
|
|
319
|
+
suggestion: 'Consider using .contextignore to exclude unnecessary files',
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
// Check cache effectiveness
|
|
324
|
+
if (metrics.cacheHitRate < 50 && metrics.totalAnalyses > 2) {
|
|
325
|
+
bottlenecks.push({
|
|
326
|
+
type: 'cache',
|
|
327
|
+
name: 'Low cache hit rate',
|
|
328
|
+
rate: metrics.cacheHitRate,
|
|
329
|
+
severity: 'medium',
|
|
330
|
+
suggestion: 'Cache warming may improve performance',
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
// Sort by severity
|
|
335
|
+
const severityOrder = { high: 0, medium: 1, low: 2 };
|
|
336
|
+
return bottlenecks.sort((a, b) => severityOrder[a.severity] - severityOrder[b.severity]);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
/**
|
|
340
|
+
* Generate optimization recommendations
|
|
341
|
+
* @param {object} metrics - Aggregated metrics
|
|
342
|
+
* @param {object[]} bottlenecks - Identified bottlenecks
|
|
343
|
+
*/
|
|
344
|
+
generateRecommendations(metrics, bottlenecks) {
|
|
345
|
+
const recommendations = [];
|
|
346
|
+
|
|
347
|
+
// Based on bottlenecks
|
|
348
|
+
for (const b of bottlenecks) {
|
|
349
|
+
if (b.suggestion) {
|
|
350
|
+
recommendations.push({
|
|
351
|
+
type: 'performance',
|
|
352
|
+
priority: b.severity === 'high' ? 'high' : 'medium',
|
|
353
|
+
message: b.suggestion,
|
|
354
|
+
details: b,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
// Based on token counts
|
|
360
|
+
if (metrics.avgTokens > 50000) {
|
|
361
|
+
recommendations.push({
|
|
362
|
+
type: 'context',
|
|
363
|
+
priority: 'medium',
|
|
364
|
+
message: 'Consider chunking for large context outputs',
|
|
365
|
+
details: { avgTokens: metrics.avgTokens },
|
|
366
|
+
});
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
// Based on file distribution
|
|
370
|
+
if (metrics.avgFiles > 500) {
|
|
371
|
+
recommendations.push({
|
|
372
|
+
type: 'filtering',
|
|
373
|
+
priority: 'low',
|
|
374
|
+
message: 'Review .contextignore for unnecessary file types',
|
|
375
|
+
details: { avgFiles: metrics.avgFiles },
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
// Cache recommendation
|
|
380
|
+
if (metrics.cacheHitRate < 30 && metrics.totalAnalyses > 1) {
|
|
381
|
+
recommendations.push({
|
|
382
|
+
type: 'cache',
|
|
383
|
+
priority: 'medium',
|
|
384
|
+
message: 'Enable caching for frequently analyzed files',
|
|
385
|
+
details: { currentRate: metrics.cacheHitRate },
|
|
386
|
+
});
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
// Remove duplicates by message
|
|
390
|
+
const seen = new Set();
|
|
391
|
+
return recommendations.filter((r) => {
|
|
392
|
+
if (seen.has(r.message)) return false;
|
|
393
|
+
seen.add(r.message);
|
|
394
|
+
return true;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/**
|
|
399
|
+
* Run analysis and collect current metrics
|
|
400
|
+
* @param {object} options - Analysis options
|
|
401
|
+
*/
|
|
402
|
+
async runAnalysis(options = {}) {
|
|
403
|
+
this.startAnalysis();
|
|
404
|
+
|
|
405
|
+
// Run discovery phase
|
|
406
|
+
const discoveryStart = Date.now();
|
|
407
|
+
const analyzer = new TokenAnalyzer(this.projectRoot, {
|
|
408
|
+
...options,
|
|
409
|
+
simple: true,
|
|
410
|
+
verbose: false,
|
|
411
|
+
dashboard: true,
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
const stats = analyzer.run();
|
|
415
|
+
this.recordPhase('discovery', Date.now() - discoveryStart, {
|
|
416
|
+
fileCount: stats.totalFiles,
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
// Record analysis phase (approximate)
|
|
420
|
+
const analysisDuration =
|
|
421
|
+
stats.largestFiles?.length > 0
|
|
422
|
+
? Math.round((stats.totalTokens / 10000) * 100) // Estimate based on token count
|
|
423
|
+
: 100;
|
|
424
|
+
this.recordPhase('analysis', analysisDuration, {
|
|
425
|
+
totalTokens: stats.totalTokens,
|
|
426
|
+
});
|
|
427
|
+
|
|
428
|
+
// End analysis
|
|
429
|
+
this.endAnalysis({
|
|
430
|
+
totalFiles: stats.totalFiles,
|
|
431
|
+
totalTokens: stats.totalTokens,
|
|
432
|
+
totalBytes: stats.totalBytes,
|
|
433
|
+
totalLines: stats.totalLines,
|
|
434
|
+
cacheHits: 0,
|
|
435
|
+
cacheMisses: 0,
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
return stats;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/**
|
|
442
|
+
* Generate ASCII bar chart
|
|
443
|
+
* @param {number} value - Current value
|
|
444
|
+
* @param {number} max - Maximum value
|
|
445
|
+
* @param {number} width - Chart width in characters
|
|
446
|
+
*/
|
|
447
|
+
generateBarChart(value, max, width = 20) {
|
|
448
|
+
if (max === 0) return '░'.repeat(width);
|
|
449
|
+
|
|
450
|
+
const ratio = Math.min(value / max, 1); // Clamp to max 1
|
|
451
|
+
const filled = Math.round(ratio * width);
|
|
452
|
+
const empty = width - filled;
|
|
453
|
+
|
|
454
|
+
return '▓'.repeat(filled) + '░'.repeat(empty);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/**
|
|
458
|
+
* Generate ASCII sparkline
|
|
459
|
+
* @param {number[]} values - Array of values
|
|
460
|
+
* @param {number} width - Sparkline width
|
|
461
|
+
*/
|
|
462
|
+
generateSparkline(values, width = 40) {
|
|
463
|
+
if (!values || values.length === 0) {
|
|
464
|
+
return '░'.repeat(width);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
const chars = ['▁', '▂', '▃', '▄', '▅', '▆', '▇', '█'];
|
|
468
|
+
const max = Math.max(...values);
|
|
469
|
+
const min = Math.min(...values);
|
|
470
|
+
const range = max - min || 1;
|
|
471
|
+
|
|
472
|
+
// Normalize and map to characters
|
|
473
|
+
const normalized = values.map((v) => {
|
|
474
|
+
const normalized = (v - min) / range;
|
|
475
|
+
const index = Math.min(Math.floor(normalized * chars.length), chars.length - 1);
|
|
476
|
+
return chars[index];
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
// Pad or trim to width
|
|
480
|
+
if (normalized.length < width) {
|
|
481
|
+
return normalized.join('') + '░'.repeat(width - normalized.length);
|
|
482
|
+
} else if (normalized.length > width) {
|
|
483
|
+
return normalized.slice(-width).join('');
|
|
484
|
+
}
|
|
485
|
+
return normalized.join('');
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
/**
|
|
489
|
+
* Generate ASCII trend chart
|
|
490
|
+
* @param {object[]} trend - Trend data points
|
|
491
|
+
* @param {string} field - Field to chart ('avgDuration', 'totalTokens', 'count')
|
|
492
|
+
* @param {number} width - Chart width
|
|
493
|
+
* @param {number} height - Chart height
|
|
494
|
+
*/
|
|
495
|
+
generateTrendChart(trend, field = 'avgDuration', width = 60, height = 6) {
|
|
496
|
+
if (!trend || trend.length === 0) {
|
|
497
|
+
return '│' + '\n'.repeat(height - 1);
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
const values = trend.map((t) => t[field] || 0);
|
|
501
|
+
const max = Math.max(...values);
|
|
502
|
+
const min = Math.min(...values);
|
|
503
|
+
const range = max - min || 1;
|
|
504
|
+
|
|
505
|
+
// Create grid
|
|
506
|
+
const grid = [];
|
|
507
|
+
for (let y = 0; y < height; y++) {
|
|
508
|
+
grid[y] = [];
|
|
509
|
+
for (let x = 0; x < width; x++) {
|
|
510
|
+
grid[y][x] = ' ';
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// Plot values
|
|
515
|
+
const step = Math.max(1, Math.floor(values.length / width));
|
|
516
|
+
for (let i = 0; i < values.length && i * step < width; i++) {
|
|
517
|
+
const x = Math.floor((i / (values.length - 1 || 1)) * (width - 1));
|
|
518
|
+
const normalized = (values[i] - min) / range;
|
|
519
|
+
const y = height - 1 - Math.floor(normalized * (height - 1));
|
|
520
|
+
|
|
521
|
+
grid[y][x] = '█';
|
|
522
|
+
|
|
523
|
+
// Fill below
|
|
524
|
+
for (let fillY = y + 1; fillY < height; fillY++) {
|
|
525
|
+
grid[fillY][x] = '░';
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
529
|
+
// Add axes
|
|
530
|
+
const lines = [];
|
|
531
|
+
for (let y = 0; y < height; y++) {
|
|
532
|
+
lines.push('│' + grid[y].join(''));
|
|
533
|
+
}
|
|
534
|
+
lines.push('└' + '─'.repeat(width));
|
|
535
|
+
|
|
536
|
+
return lines.join('\n');
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Render the full dashboard
|
|
541
|
+
* @param {object} options - Render options
|
|
542
|
+
*/
|
|
543
|
+
render(options = {}) {
|
|
544
|
+
const period = options.period || '7d';
|
|
545
|
+
const metrics = this.getAggregatedMetrics(period);
|
|
546
|
+
const bottlenecks = this.identifyBottlenecks(metrics);
|
|
547
|
+
const recommendations = this.generateRecommendations(metrics, bottlenecks);
|
|
548
|
+
|
|
549
|
+
const lines = [];
|
|
550
|
+
|
|
551
|
+
// Header
|
|
552
|
+
lines.push('');
|
|
553
|
+
lines.push('═'.repeat(70));
|
|
554
|
+
lines.push(' CTXMAN PERFORMANCE DASHBOARD');
|
|
555
|
+
lines.push('═'.repeat(70));
|
|
556
|
+
lines.push('');
|
|
557
|
+
|
|
558
|
+
// Overview metrics
|
|
559
|
+
lines.push('┌' + '─'.repeat(30) + '┬' + '─'.repeat(30) + '┐');
|
|
560
|
+
lines.push('│ Overview │ Analysis Stats │');
|
|
561
|
+
lines.push('├' + '─'.repeat(30) + '┼' + '─'.repeat(30) + '┤');
|
|
562
|
+
|
|
563
|
+
// Total analyses
|
|
564
|
+
const totalAnalysesStr = metrics.totalAnalyses.toLocaleString().padStart(10);
|
|
565
|
+
lines.push(
|
|
566
|
+
`│ Total Analyses:${totalAnalysesStr} │ Avg Duration: ${(metrics.avgDuration || 0).toLocaleString()} ms │`
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
// Average files
|
|
570
|
+
const avgFilesStr = metrics.avgFiles.toLocaleString().padStart(10);
|
|
571
|
+
lines.push(
|
|
572
|
+
`│ Avg Files: ${avgFilesStr} │ Avg Tokens: ${(metrics.avgTokens || 0).toLocaleString().padStart(10)} │`
|
|
573
|
+
);
|
|
574
|
+
|
|
575
|
+
// Cache hit rate
|
|
576
|
+
const cacheBar = this.generateBarChart(metrics.cacheHitRate, 100, 10);
|
|
577
|
+
lines.push(
|
|
578
|
+
`│ Cache Hit Rate: ${cacheBar} ${String(metrics.cacheHitRate).padStart(3)}% │ │`
|
|
579
|
+
);
|
|
580
|
+
|
|
581
|
+
lines.push('└' + '─'.repeat(30) + '┴' + '─'.repeat(30) + '┘');
|
|
582
|
+
lines.push('');
|
|
583
|
+
|
|
584
|
+
// Performance trend
|
|
585
|
+
if (metrics.trend && metrics.trend.length > 0) {
|
|
586
|
+
lines.push('┌' + '─'.repeat(66) + '┐');
|
|
587
|
+
lines.push(
|
|
588
|
+
'│ Performance Trend (Last ' +
|
|
589
|
+
String(metrics.trend.length).padStart(2) +
|
|
590
|
+
' days)' +
|
|
591
|
+
' '.repeat(38) +
|
|
592
|
+
'│'
|
|
593
|
+
);
|
|
594
|
+
lines.push('├' + '─'.repeat(66) + '┤');
|
|
595
|
+
|
|
596
|
+
// Duration sparkline
|
|
597
|
+
const durationValues = metrics.trend.map((t) => t.avgDuration);
|
|
598
|
+
const durationSparkline = this.generateSparkline(durationValues, 40);
|
|
599
|
+
lines.push(`│ Duration (ms) ${durationSparkline} │`);
|
|
600
|
+
|
|
601
|
+
// Token sparkline
|
|
602
|
+
const tokenValues = metrics.trend.map((t) => Math.round(t.totalTokens / 1000));
|
|
603
|
+
const tokenSparkline = this.generateSparkline(tokenValues, 40);
|
|
604
|
+
lines.push(`│ Tokens (K) ${tokenSparkline} │`);
|
|
605
|
+
|
|
606
|
+
// Analysis count sparkline
|
|
607
|
+
const countValues = metrics.trend.map((t) => t.count);
|
|
608
|
+
const countSparkline = this.generateSparkline(countValues, 40);
|
|
609
|
+
lines.push(`│ Analyses ${countSparkline} │`);
|
|
610
|
+
|
|
611
|
+
lines.push('└' + '─'.repeat(66) + '┘');
|
|
612
|
+
lines.push('');
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Phase breakdown
|
|
616
|
+
if (Object.keys(metrics.phaseBreakdown).length > 0) {
|
|
617
|
+
lines.push('┌' + '─'.repeat(66) + '┐');
|
|
618
|
+
lines.push('│ Phase Breakdown │');
|
|
619
|
+
lines.push('├' + '─'.repeat(66) + '┤');
|
|
620
|
+
|
|
621
|
+
const phases = Object.entries(metrics.phaseBreakdown).sort(
|
|
622
|
+
(a, b) => b[1].avgDuration - a[1].avgDuration
|
|
623
|
+
);
|
|
624
|
+
|
|
625
|
+
for (const [name, data] of phases) {
|
|
626
|
+
const bar = this.generateBarChart(data.avgDuration, metrics.avgDuration || 1, 20);
|
|
627
|
+
const pct =
|
|
628
|
+
metrics.avgDuration > 0 ? Math.round((data.avgDuration / metrics.avgDuration) * 100) : 0;
|
|
629
|
+
lines.push(
|
|
630
|
+
`│ ${(name + ':').padEnd(15)} ${bar} ${String(data.avgDuration).padStart(6)} ms (${String(pct).padStart(3)}%) │`
|
|
631
|
+
);
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
lines.push('└' + '─'.repeat(66) + '┘');
|
|
635
|
+
lines.push('');
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
// Bottlenecks
|
|
639
|
+
if (bottlenecks.length > 0) {
|
|
640
|
+
lines.push('┌' + '─'.repeat(66) + '┐');
|
|
641
|
+
lines.push('│ Top Bottlenecks │');
|
|
642
|
+
lines.push('├' + '─'.repeat(66) + '┤');
|
|
643
|
+
|
|
644
|
+
for (let i = 0; i < Math.min(bottlenecks.length, 5); i++) {
|
|
645
|
+
const b = bottlenecks[i];
|
|
646
|
+
const severity = b.severity === 'high' ? '⚠️ ' : ' ';
|
|
647
|
+
const name = b.name.padEnd(40);
|
|
648
|
+
const info = b.avgDuration ? `${b.avgDuration} ms avg` : b.rate ? `${b.rate}%` : '';
|
|
649
|
+
lines.push(`│ ${severity}${i + 1}. ${name} ${info.padStart(15)} │`);
|
|
650
|
+
}
|
|
651
|
+
|
|
652
|
+
lines.push('└' + '─'.repeat(66) + '┘');
|
|
653
|
+
lines.push('');
|
|
654
|
+
}
|
|
655
|
+
|
|
656
|
+
// Recommendations
|
|
657
|
+
if (recommendations.length > 0) {
|
|
658
|
+
lines.push('┌' + '─'.repeat(66) + '┐');
|
|
659
|
+
lines.push('│ Optimization Recommendations │');
|
|
660
|
+
lines.push('├' + '─'.repeat(66) + '┤');
|
|
661
|
+
|
|
662
|
+
for (const r of recommendations.slice(0, 5)) {
|
|
663
|
+
const priority = r.priority === 'high' ? '🔴' : r.priority === 'medium' ? '🟡' : '🟢';
|
|
664
|
+
lines.push(`│ ${priority} ${r.message.slice(0, 60).padEnd(60)} │`);
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
lines.push('└' + '─'.repeat(66) + '┘');
|
|
668
|
+
lines.push('');
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
// Legend
|
|
672
|
+
lines.push('Legend: ▓ active ░ inactive █ high ▇ medium ▆ low');
|
|
673
|
+
lines.push('');
|
|
674
|
+
|
|
675
|
+
return lines.join('\n');
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
/**
|
|
679
|
+
* Display dashboard to console
|
|
680
|
+
* @param {object} options - Display options
|
|
681
|
+
*/
|
|
682
|
+
display(options = {}) {
|
|
683
|
+
console.log(this.render(options));
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
export default PerformanceDashboard;
|