@lydt/geogebra-studio 0.0.0-stage → 0.4.0-rc.2
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/.agents/plugins/marketplace.json +21 -0
- package/.codex-plugin/plugin.json +21 -0
- package/.mcp.json +18 -0
- package/LICENSE +339 -0
- package/LICENSE-MIT +21 -0
- package/README.md +108 -2
- package/README.zh-CN.md +89 -0
- package/THIRD_PARTY_NOTICES.md +35 -0
- package/dist/cli.d.ts +27 -0
- package/dist/cli.d.ts.map +1 -0
- package/dist/cli.js +234 -0
- package/dist/cli.js.map +1 -0
- package/dist/export/files.d.ts +4 -0
- package/dist/export/files.d.ts.map +1 -0
- package/dist/export/files.js +44 -0
- package/dist/export/files.js.map +1 -0
- package/dist/export/gikz.d.ts +11 -0
- package/dist/export/gikz.d.ts.map +1 -0
- package/dist/export/gikz.js +28 -0
- package/dist/export/gikz.js.map +1 -0
- package/dist/export/html.d.ts +14 -0
- package/dist/export/html.d.ts.map +1 -0
- package/dist/export/html.js +97 -0
- package/dist/export/html.js.map +1 -0
- package/dist/export/semantic.d.ts +43 -0
- package/dist/export/semantic.d.ts.map +1 -0
- package/dist/export/semantic.js +212 -0
- package/dist/export/semantic.js.map +1 -0
- package/dist/export/viewer-themes.d.ts +76 -0
- package/dist/export/viewer-themes.d.ts.map +1 -0
- package/dist/export/viewer-themes.js +169 -0
- package/dist/export/viewer-themes.js.map +1 -0
- package/dist/export/workspace.d.ts +4 -0
- package/dist/export/workspace.d.ts.map +1 -0
- package/dist/export/workspace.js +25 -0
- package/dist/export/workspace.js.map +1 -0
- package/dist/gikz-browser.js +770 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +84 -0
- package/dist/index.js.map +1 -0
- package/dist/server.d.ts +35 -0
- package/dist/server.d.ts.map +1 -0
- package/dist/server.js +187 -0
- package/dist/server.js.map +1 -0
- package/dist/studio/artifact.d.ts +37 -0
- package/dist/studio/artifact.d.ts.map +1 -0
- package/dist/studio/artifact.js +61 -0
- package/dist/studio/artifact.js.map +1 -0
- package/dist/studio/browser.d.ts +7 -0
- package/dist/studio/browser.d.ts.map +1 -0
- package/dist/studio/browser.js +67 -0
- package/dist/studio/browser.js.map +1 -0
- package/dist/studio/install.d.ts +4 -0
- package/dist/studio/install.d.ts.map +1 -0
- package/dist/studio/install.js +60 -0
- package/dist/studio/install.js.map +1 -0
- package/dist/tools/apps-api-tools.d.ts +4 -0
- package/dist/tools/apps-api-tools.d.ts.map +1 -0
- package/dist/tools/apps-api-tools.js +96 -0
- package/dist/tools/apps-api-tools.js.map +1 -0
- package/dist/tools/educational-templates.d.ts +33 -0
- package/dist/tools/educational-templates.d.ts.map +1 -0
- package/dist/tools/educational-templates.js +261 -0
- package/dist/tools/educational-templates.js.map +1 -0
- package/dist/tools/export-tools.d.ts +3 -0
- package/dist/tools/export-tools.d.ts.map +1 -0
- package/dist/tools/export-tools.js +118 -0
- package/dist/tools/export-tools.js.map +1 -0
- package/dist/tools/geogebra-tools.d.ts +15 -0
- package/dist/tools/geogebra-tools.d.ts.map +1 -0
- package/dist/tools/geogebra-tools.js +3442 -0
- package/dist/tools/geogebra-tools.js.map +1 -0
- package/dist/tools/index.d.ts +43 -0
- package/dist/tools/index.d.ts.map +1 -0
- package/dist/tools/index.js +183 -0
- package/dist/tools/index.js.map +1 -0
- package/dist/tools/performance-tools.d.ts +7 -0
- package/dist/tools/performance-tools.d.ts.map +1 -0
- package/dist/tools/performance-tools.js +312 -0
- package/dist/tools/performance-tools.js.map +1 -0
- package/dist/tools/templates/algebra-templates.d.ts +7 -0
- package/dist/tools/templates/algebra-templates.d.ts.map +1 -0
- package/dist/tools/templates/algebra-templates.js +568 -0
- package/dist/tools/templates/algebra-templates.js.map +1 -0
- package/dist/tools/templates/geometry-templates.d.ts +7 -0
- package/dist/tools/templates/geometry-templates.d.ts.map +1 -0
- package/dist/tools/templates/geometry-templates.js +492 -0
- package/dist/tools/templates/geometry-templates.js.map +1 -0
- package/dist/types/geogebra.d.ts +103 -0
- package/dist/types/geogebra.d.ts.map +1 -0
- package/dist/types/geogebra.js +34 -0
- package/dist/types/geogebra.js.map +1 -0
- package/dist/types/mcp.d.ts +125 -0
- package/dist/types/mcp.d.ts.map +1 -0
- package/dist/types/mcp.js +19 -0
- package/dist/types/mcp.js.map +1 -0
- package/dist/utils/app-info.d.ts +12 -0
- package/dist/utils/app-info.d.ts.map +1 -0
- package/dist/utils/app-info.js +30 -0
- package/dist/utils/app-info.js.map +1 -0
- package/dist/utils/errors.d.ts +37 -0
- package/dist/utils/errors.d.ts.map +1 -0
- package/dist/utils/errors.js +72 -0
- package/dist/utils/errors.js.map +1 -0
- package/dist/utils/gemini-compatibility.d.ts +39 -0
- package/dist/utils/gemini-compatibility.d.ts.map +1 -0
- package/dist/utils/gemini-compatibility.js +154 -0
- package/dist/utils/gemini-compatibility.js.map +1 -0
- package/dist/utils/geogebra-instance.d.ts +252 -0
- package/dist/utils/geogebra-instance.d.ts.map +1 -0
- package/dist/utils/geogebra-instance.js +1560 -0
- package/dist/utils/geogebra-instance.js.map +1 -0
- package/dist/utils/geogebra-mock.d.ts +172 -0
- package/dist/utils/geogebra-mock.d.ts.map +1 -0
- package/dist/utils/geogebra-mock.js +732 -0
- package/dist/utils/geogebra-mock.js.map +1 -0
- package/dist/utils/logger.d.ts +4 -0
- package/dist/utils/logger.d.ts.map +1 -0
- package/dist/utils/logger.js +46 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/performance/index.d.ts +76 -0
- package/dist/utils/performance/index.d.ts.map +1 -0
- package/dist/utils/performance/index.js +163 -0
- package/dist/utils/performance/index.js.map +1 -0
- package/dist/utils/performance/instance-pool.d.ts +78 -0
- package/dist/utils/performance/instance-pool.d.ts.map +1 -0
- package/dist/utils/performance/instance-pool.js +268 -0
- package/dist/utils/performance/instance-pool.js.map +1 -0
- package/dist/utils/validation.d.ts +92 -0
- package/dist/utils/validation.d.ts.map +1 -0
- package/dist/utils/validation.js +557 -0
- package/dist/utils/validation.js.map +1 -0
- package/dist/widget.html +110 -0
- package/docs/APPS-API-BRIDGE.md +28 -0
- package/docs/MODERNIZATION.md +15 -0
- package/docs/TANGENT-REGRESSION.md +34 -0
- package/docs/VALIDATION.md +26 -0
- package/docs/VISUAL-STANDARD.md +26 -0
- package/docs/images/native-construction.png +0 -0
- package/examples/tangent-construction.json +26 -0
- package/gikz/LICENSE +339 -0
- package/gikz/PROVENANCE.md +8 -0
- package/gikz/browser.cjs +2 -0
- package/gikz/cli.cjs +195 -0
- package/gikz/core.cjs +806 -0
- package/mcp.json +20 -0
- package/package.json +107 -4
- package/plugin.json +35 -0
- package/runtime/package.json +4 -0
- package/scripts/build-plugin.cjs +15 -0
- package/scripts/build-widget.cjs +7 -0
- package/skills/geogebra-construct/SKILL.md +40 -0
- package/skills/geogebra-setup/SKILL.md +37 -0
- package/skills/geometry-export/SKILL.md +31 -0
- package/src/cli.ts +212 -0
- package/src/export/files.ts +26 -0
- package/src/export/gikz.ts +22 -0
- package/src/export/html.ts +108 -0
- package/src/export/semantic.ts +180 -0
- package/src/export/viewer-themes.ts +165 -0
- package/src/export/workspace.ts +15 -0
- package/src/index.ts +71 -0
- package/src/server.ts +168 -0
- package/src/studio/artifact.ts +50 -0
- package/src/studio/browser.ts +38 -0
- package/src/studio/install.ts +43 -0
- package/src/tools/apps-api-tools.ts +96 -0
- package/src/tools/educational-templates.ts +299 -0
- package/src/tools/export-tools.ts +110 -0
- package/src/tools/geogebra-tools.ts +3836 -0
- package/src/tools/index.ts +192 -0
- package/src/tools/performance-tools.ts +322 -0
- package/src/tools/templates/algebra-templates.ts +619 -0
- package/src/tools/templates/geometry-templates.ts +550 -0
- package/src/types/geogebra.ts +140 -0
- package/src/types/global.d.ts +31 -0
- package/src/types/mcp.ts +141 -0
- package/src/utils/app-info.ts +35 -0
- package/src/utils/errors.ts +97 -0
- package/src/utils/gemini-compatibility.ts +193 -0
- package/src/utils/geogebra-instance.ts +1712 -0
- package/src/utils/geogebra-mock.ts +867 -0
- package/src/utils/logger.ts +60 -0
- package/src/utils/performance/index.ts +211 -0
- package/src/utils/performance/instance-pool.ts +323 -0
- package/src/utils/validation.ts +647 -0
- package/tsconfig.json +35 -0
- package/widget/main.ts +12 -0
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import winston from 'winston';
|
|
2
|
+
|
|
3
|
+
interface LogInfo {
|
|
4
|
+
timestamp: string;
|
|
5
|
+
level: string;
|
|
6
|
+
message: string;
|
|
7
|
+
stack?: string;
|
|
8
|
+
[key: string]: unknown;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Check if we're in MCP mode (stdio communication)
|
|
12
|
+
// When piping input, process.stdin.isTTY is undefined, not false
|
|
13
|
+
const isMcpMode = !process.stdin.isTTY;
|
|
14
|
+
|
|
15
|
+
// Create logger instance
|
|
16
|
+
const logger = winston.createLogger({
|
|
17
|
+
level: isMcpMode ? 'error' : (process.env['LOG_LEVEL'] || 'info'),
|
|
18
|
+
format: winston.format.combine(
|
|
19
|
+
winston.format.timestamp({
|
|
20
|
+
format: 'YYYY-MM-DD HH:mm:ss'
|
|
21
|
+
}),
|
|
22
|
+
winston.format.errors({ stack: true }),
|
|
23
|
+
winston.format.printf((info) => {
|
|
24
|
+
const { timestamp, level, message, stack, ...meta } = info as unknown as LogInfo;
|
|
25
|
+
let log = `${timestamp} [${level.toUpperCase()}]: ${message}`;
|
|
26
|
+
|
|
27
|
+
if (Object.keys(meta).length > 0) {
|
|
28
|
+
log += ` ${JSON.stringify(meta)}`;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
if (stack) {
|
|
32
|
+
log += `\n${stack}`;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
return log;
|
|
36
|
+
})
|
|
37
|
+
),
|
|
38
|
+
defaultMeta: { service: 'geogebra-mcp-server' },
|
|
39
|
+
transports: [
|
|
40
|
+
// Use stderr instead of stdout to avoid interfering with MCP JSON-RPC communication
|
|
41
|
+
new winston.transports.Console({
|
|
42
|
+
stderrLevels: ['error', 'warn', 'info', 'debug'],
|
|
43
|
+
format: winston.format.simple() // Remove colorization for MCP compatibility
|
|
44
|
+
})
|
|
45
|
+
]
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// Add file transport in production
|
|
49
|
+
if (process.env['NODE_ENV'] === 'production') {
|
|
50
|
+
logger.add(new winston.transports.File({
|
|
51
|
+
filename: 'logs/error.log',
|
|
52
|
+
level: 'error'
|
|
53
|
+
}));
|
|
54
|
+
|
|
55
|
+
logger.add(new winston.transports.File({
|
|
56
|
+
filename: 'logs/combined.log'
|
|
57
|
+
}));
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export default logger;
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Performance Monitoring and Optimization System
|
|
3
|
+
* GEB-9: Performance Optimization: Response Time and Resource Management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface PerformanceMetrics {
|
|
7
|
+
operationName: string;
|
|
8
|
+
startTime: number;
|
|
9
|
+
endTime: number;
|
|
10
|
+
duration: number;
|
|
11
|
+
success: boolean;
|
|
12
|
+
errorMessage?: string;
|
|
13
|
+
memoryUsage?: NodeJS.MemoryUsage;
|
|
14
|
+
metadata?: Record<string, any>;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PerformanceThresholds {
|
|
18
|
+
[operationName: string]: {
|
|
19
|
+
warning: number; // milliseconds
|
|
20
|
+
critical: number; // milliseconds
|
|
21
|
+
};
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export class PerformanceMonitor {
|
|
25
|
+
private static instance: PerformanceMonitor;
|
|
26
|
+
private metrics: PerformanceMetrics[] = [];
|
|
27
|
+
private thresholds: PerformanceThresholds;
|
|
28
|
+
private maxMetrics: number = 1000; // Keep last 1000 metrics
|
|
29
|
+
|
|
30
|
+
// PRD requirements: <2 second response time
|
|
31
|
+
private defaultThresholds: PerformanceThresholds = {
|
|
32
|
+
'geogebra_eval_command': { warning: 1000, critical: 2000 },
|
|
33
|
+
'geogebra_create_point': { warning: 500, critical: 1000 },
|
|
34
|
+
'geogebra_create_line': { warning: 500, critical: 1000 },
|
|
35
|
+
'geogebra_export_png': { warning: 1500, critical: 2000 },
|
|
36
|
+
'geogebra_export_svg': { warning: 800, critical: 1500 },
|
|
37
|
+
'geogebra_instance_init': { warning: 8000, critical: 15000 },
|
|
38
|
+
'geogebra_clear_construction': { warning: 300, critical: 1000 },
|
|
39
|
+
'default': { warning: 1000, critical: 2000 }
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
private constructor() {
|
|
43
|
+
this.thresholds = { ...this.defaultThresholds };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
static getInstance(): PerformanceMonitor {
|
|
47
|
+
if (!PerformanceMonitor.instance) {
|
|
48
|
+
PerformanceMonitor.instance = new PerformanceMonitor();
|
|
49
|
+
}
|
|
50
|
+
return PerformanceMonitor.instance;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Start timing an operation
|
|
55
|
+
*/
|
|
56
|
+
startTiming(operationName: string, metadata?: Record<string, any>): (success?: boolean, errorMessage?: string) => PerformanceMetrics {
|
|
57
|
+
const startTime = performance.now();
|
|
58
|
+
const startMemory = process.memoryUsage();
|
|
59
|
+
|
|
60
|
+
return (success: boolean = true, errorMessage?: string) => {
|
|
61
|
+
const endTime = performance.now();
|
|
62
|
+
const endMemory = process.memoryUsage();
|
|
63
|
+
const duration = endTime - startTime;
|
|
64
|
+
|
|
65
|
+
const metric: PerformanceMetrics = {
|
|
66
|
+
operationName,
|
|
67
|
+
startTime,
|
|
68
|
+
endTime,
|
|
69
|
+
duration,
|
|
70
|
+
success,
|
|
71
|
+
...(errorMessage && { errorMessage }),
|
|
72
|
+
memoryUsage: {
|
|
73
|
+
rss: endMemory.rss - startMemory.rss,
|
|
74
|
+
heapTotal: endMemory.heapTotal - startMemory.heapTotal,
|
|
75
|
+
heapUsed: endMemory.heapUsed - startMemory.heapUsed,
|
|
76
|
+
external: endMemory.external - startMemory.external,
|
|
77
|
+
arrayBuffers: endMemory.arrayBuffers - startMemory.arrayBuffers
|
|
78
|
+
},
|
|
79
|
+
...(metadata && { metadata })
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
this.addMetric(metric);
|
|
83
|
+
this.checkThresholds(metric);
|
|
84
|
+
return metric;
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Measure and record an async operation
|
|
90
|
+
*/
|
|
91
|
+
async measureOperation<T>(
|
|
92
|
+
operationName: string,
|
|
93
|
+
operation: () => Promise<T>,
|
|
94
|
+
metadata?: Record<string, any>
|
|
95
|
+
): Promise<T> {
|
|
96
|
+
const endTiming = this.startTiming(operationName, metadata);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
const result = await operation();
|
|
100
|
+
endTiming(true);
|
|
101
|
+
return result;
|
|
102
|
+
} catch (error) {
|
|
103
|
+
endTiming(false, error instanceof Error ? error.message : String(error));
|
|
104
|
+
throw error;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Add a metric to the collection
|
|
110
|
+
*/
|
|
111
|
+
private addMetric(metric: PerformanceMetrics): void {
|
|
112
|
+
this.metrics.push(metric);
|
|
113
|
+
|
|
114
|
+
// Keep only the last maxMetrics entries
|
|
115
|
+
if (this.metrics.length > this.maxMetrics) {
|
|
116
|
+
this.metrics = this.metrics.slice(-this.maxMetrics);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Check if metric exceeds thresholds and log warnings
|
|
122
|
+
*/
|
|
123
|
+
private checkThresholds(metric: PerformanceMetrics): void {
|
|
124
|
+
const threshold = this.thresholds[metric.operationName] || this.thresholds['default'];
|
|
125
|
+
|
|
126
|
+
if (threshold && metric.duration > threshold.critical) {
|
|
127
|
+
console.error(`🚨 CRITICAL: ${metric.operationName} took ${metric.duration.toFixed(2)}ms (threshold: ${threshold.critical}ms)`);
|
|
128
|
+
} else if (threshold && metric.duration > threshold.warning) {
|
|
129
|
+
console.warn(`⚠️ WARNING: ${metric.operationName} took ${metric.duration.toFixed(2)}ms (threshold: ${threshold.warning}ms)`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get performance statistics for an operation
|
|
135
|
+
*/
|
|
136
|
+
getStats(operationName?: string): {
|
|
137
|
+
count: number;
|
|
138
|
+
averageDuration: number;
|
|
139
|
+
medianDuration: number;
|
|
140
|
+
minDuration: number;
|
|
141
|
+
maxDuration: number;
|
|
142
|
+
successRate: number;
|
|
143
|
+
p95Duration: number;
|
|
144
|
+
p99Duration: number;
|
|
145
|
+
} {
|
|
146
|
+
const relevantMetrics = operationName
|
|
147
|
+
? this.metrics.filter(m => m.operationName === operationName)
|
|
148
|
+
: this.metrics;
|
|
149
|
+
|
|
150
|
+
if (relevantMetrics.length === 0) {
|
|
151
|
+
return {
|
|
152
|
+
count: 0,
|
|
153
|
+
averageDuration: 0,
|
|
154
|
+
medianDuration: 0,
|
|
155
|
+
minDuration: 0,
|
|
156
|
+
maxDuration: 0,
|
|
157
|
+
successRate: 0,
|
|
158
|
+
p95Duration: 0,
|
|
159
|
+
p99Duration: 0
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const durations = relevantMetrics.map(m => m.duration).sort((a, b) => a - b);
|
|
164
|
+
const successCount = relevantMetrics.filter(m => m.success).length;
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
count: relevantMetrics.length,
|
|
168
|
+
averageDuration: durations.reduce((sum, d) => sum + d, 0) / durations.length,
|
|
169
|
+
medianDuration:
|
|
170
|
+
durations.length % 2 === 0
|
|
171
|
+
? ((durations[durations.length / 2 - 1] ?? 0) + (durations[durations.length / 2] ?? 0)) / 2
|
|
172
|
+
: durations[Math.floor(durations.length / 2)] ?? 0,
|
|
173
|
+
minDuration: durations[0] || 0,
|
|
174
|
+
maxDuration: durations[durations.length - 1] || 0,
|
|
175
|
+
successRate: successCount / relevantMetrics.length,
|
|
176
|
+
p95Duration: durations[Math.floor(durations.length * 0.95)] || 0,
|
|
177
|
+
p99Duration: durations[Math.floor(durations.length * 0.99)] || 0
|
|
178
|
+
};
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* Get all operation names with metrics
|
|
183
|
+
*/
|
|
184
|
+
getOperationNames(): string[] {
|
|
185
|
+
return [...new Set(this.metrics.map(m => m.operationName))];
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Clear all metrics
|
|
190
|
+
*/
|
|
191
|
+
clearMetrics(): void {
|
|
192
|
+
this.metrics = [];
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Export metrics for analysis
|
|
197
|
+
*/
|
|
198
|
+
exportMetrics(): PerformanceMetrics[] {
|
|
199
|
+
return [...this.metrics];
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Set custom thresholds
|
|
204
|
+
*/
|
|
205
|
+
setThresholds(thresholds: Partial<PerformanceThresholds>): void {
|
|
206
|
+
Object.assign(this.thresholds, thresholds);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// Global instance
|
|
211
|
+
export const performanceMonitor = PerformanceMonitor.getInstance();
|
|
@@ -0,0 +1,323 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Optimized GeoGebra Instance Pool Management
|
|
3
|
+
* GEB-9: Performance Optimization: Response Time and Resource Management
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import { GeoGebraInstance } from '../geogebra-instance';
|
|
7
|
+
import { GeoGebraPoolConfig } from '../../types/geogebra';
|
|
8
|
+
import logger from '../logger';
|
|
9
|
+
import { performanceMonitor } from './index';
|
|
10
|
+
|
|
11
|
+
// Check if we're in MCP mode (stdio communication)
|
|
12
|
+
// When piping input, process.stdin.isTTY is undefined, not false
|
|
13
|
+
const isMcpMode = !process.stdin.isTTY;
|
|
14
|
+
|
|
15
|
+
export interface PooledInstance {
|
|
16
|
+
instance: GeoGebraInstance;
|
|
17
|
+
isActive: boolean;
|
|
18
|
+
lastUsed: Date;
|
|
19
|
+
createdAt: Date;
|
|
20
|
+
usageCount: number;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export class OptimizedInstancePool {
|
|
24
|
+
private static poolInstance: OptimizedInstancePool;
|
|
25
|
+
private instances: Map<string, PooledInstance> = new Map();
|
|
26
|
+
private config: GeoGebraPoolConfig;
|
|
27
|
+
private cleanupInterval?: NodeJS.Timeout | undefined;
|
|
28
|
+
|
|
29
|
+
private defaultConfig: GeoGebraPoolConfig = {
|
|
30
|
+
maxInstances: 3, // Maximum concurrent instances
|
|
31
|
+
instanceTimeout: 300000, // 5 minutes
|
|
32
|
+
maxIdleTime: 600000, // 10 minutes
|
|
33
|
+
headless: true,
|
|
34
|
+
browserArgs: [
|
|
35
|
+
'--no-sandbox',
|
|
36
|
+
'--disable-setuid-sandbox',
|
|
37
|
+
'--disable-dev-shm-usage',
|
|
38
|
+
'--disable-accelerated-2d-canvas',
|
|
39
|
+
'--no-first-run',
|
|
40
|
+
'--no-zygote',
|
|
41
|
+
'--disable-gpu',
|
|
42
|
+
'--memory-pressure-off', // Reduce memory pressure
|
|
43
|
+
'--max_old_space_size=512' // Limit memory usage
|
|
44
|
+
]
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
private constructor(config?: Partial<GeoGebraPoolConfig>) {
|
|
48
|
+
this.config = { ...this.defaultConfig, ...config };
|
|
49
|
+
this.startCleanupTimer();
|
|
50
|
+
|
|
51
|
+
// Transport lifecycle owns graceful cleanup.
|
|
52
|
+
|
|
53
|
+
if (!isMcpMode) {
|
|
54
|
+
logger.info('Optimized Instance Pool initialized', { config: this.config });
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
static getInstance(config?: Partial<GeoGebraPoolConfig>): OptimizedInstancePool {
|
|
59
|
+
if (!OptimizedInstancePool.poolInstance) {
|
|
60
|
+
OptimizedInstancePool.poolInstance = new OptimizedInstancePool(config);
|
|
61
|
+
}
|
|
62
|
+
return OptimizedInstancePool.poolInstance;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Get an available instance from the pool
|
|
67
|
+
*/
|
|
68
|
+
async getInstance(): Promise<GeoGebraInstance> {
|
|
69
|
+
return performanceMonitor.measureOperation('instance_pool_get', async () => {
|
|
70
|
+
// Try to find an available instance
|
|
71
|
+
let availableInstance = this.findAvailableInstance();
|
|
72
|
+
|
|
73
|
+
if (!availableInstance) {
|
|
74
|
+
// Create new instance if under limit
|
|
75
|
+
if (this.instances.size < this.config.maxInstances) {
|
|
76
|
+
availableInstance = await this.createNewInstance();
|
|
77
|
+
} else {
|
|
78
|
+
// Wait for an instance to become available or force cleanup
|
|
79
|
+
await this.forceCleanupOldest();
|
|
80
|
+
availableInstance = await this.createNewInstance();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// Mark as active and update usage stats
|
|
85
|
+
const pooled = this.instances.get(availableInstance.id)!;
|
|
86
|
+
pooled.isActive = true;
|
|
87
|
+
pooled.lastUsed = new Date();
|
|
88
|
+
pooled.usageCount++;
|
|
89
|
+
|
|
90
|
+
logger.debug(`Instance ${availableInstance.id} acquired from pool`, {
|
|
91
|
+
totalInstances: this.instances.size,
|
|
92
|
+
activeInstances: this.getActiveCount(),
|
|
93
|
+
usageCount: pooled.usageCount
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
return availableInstance;
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Release an instance back to the pool
|
|
102
|
+
*/
|
|
103
|
+
async releaseInstance(instance: GeoGebraInstance): Promise<void> {
|
|
104
|
+
return performanceMonitor.measureOperation('instance_pool_release', async () => {
|
|
105
|
+
const pooled = this.instances.get(instance.id);
|
|
106
|
+
if (pooled) {
|
|
107
|
+
pooled.isActive = false;
|
|
108
|
+
pooled.lastUsed = new Date();
|
|
109
|
+
|
|
110
|
+
// Clear construction to reset state for next use
|
|
111
|
+
try {
|
|
112
|
+
await instance.newConstruction();
|
|
113
|
+
} catch (error) {
|
|
114
|
+
logger.warn(`Failed to clear construction on release: ${error}`);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
logger.debug(`Instance ${instance.id} released to pool`, {
|
|
118
|
+
totalInstances: this.instances.size,
|
|
119
|
+
activeInstances: this.getActiveCount()
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Find an available (non-active) instance
|
|
127
|
+
*/
|
|
128
|
+
private findAvailableInstance(): GeoGebraInstance | null {
|
|
129
|
+
for (const [, pooled] of this.instances) {
|
|
130
|
+
if (!pooled.isActive) {
|
|
131
|
+
return pooled.instance;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Create a new instance and add to pool
|
|
139
|
+
*/
|
|
140
|
+
private async createNewInstance(): Promise<GeoGebraInstance> {
|
|
141
|
+
return performanceMonitor.measureOperation('instance_pool_create', async () => {
|
|
142
|
+
const instance = new GeoGebraInstance({
|
|
143
|
+
appName: 'classic',
|
|
144
|
+
width: 800,
|
|
145
|
+
height: 600,
|
|
146
|
+
showMenuBar: false,
|
|
147
|
+
showToolBar: false,
|
|
148
|
+
showAlgebraInput: false
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
await instance.initialize(this.config.headless, this.config.browserArgs);
|
|
152
|
+
|
|
153
|
+
const pooled: PooledInstance = {
|
|
154
|
+
instance,
|
|
155
|
+
isActive: false,
|
|
156
|
+
lastUsed: new Date(),
|
|
157
|
+
createdAt: new Date(),
|
|
158
|
+
usageCount: 0
|
|
159
|
+
};
|
|
160
|
+
|
|
161
|
+
this.instances.set(instance.id, pooled);
|
|
162
|
+
|
|
163
|
+
logger.info(`New instance ${instance.id} created and added to pool`, {
|
|
164
|
+
totalInstances: this.instances.size
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
return instance;
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Force cleanup of the oldest instance to make room
|
|
173
|
+
*/
|
|
174
|
+
private async forceCleanupOldest(): Promise<void> {
|
|
175
|
+
let oldestId: string | null = null;
|
|
176
|
+
let oldestTime = Date.now();
|
|
177
|
+
|
|
178
|
+
// Find oldest inactive instance
|
|
179
|
+
for (const [id, pooled] of this.instances) {
|
|
180
|
+
if (!pooled.isActive && pooled.lastUsed.getTime() < oldestTime) {
|
|
181
|
+
oldestTime = pooled.lastUsed.getTime();
|
|
182
|
+
oldestId = id;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// If no inactive instance, find oldest active one
|
|
187
|
+
if (!oldestId) {
|
|
188
|
+
for (const [id, pooled] of this.instances) {
|
|
189
|
+
if (pooled.lastUsed.getTime() < oldestTime) {
|
|
190
|
+
oldestTime = pooled.lastUsed.getTime();
|
|
191
|
+
oldestId = id;
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (oldestId) {
|
|
197
|
+
await this.removeInstance(oldestId);
|
|
198
|
+
logger.info(`Forced cleanup of oldest instance ${oldestId} to make room`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* Remove and cleanup an instance
|
|
204
|
+
*/
|
|
205
|
+
private async removeInstance(instanceId: string): Promise<void> {
|
|
206
|
+
const pooled = this.instances.get(instanceId);
|
|
207
|
+
if (pooled) {
|
|
208
|
+
try {
|
|
209
|
+
await pooled.instance.cleanup();
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error(`Error cleaning up instance ${instanceId}:`, error);
|
|
212
|
+
}
|
|
213
|
+
this.instances.delete(instanceId);
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Start automatic cleanup timer
|
|
219
|
+
*/
|
|
220
|
+
private startCleanupTimer(): void {
|
|
221
|
+
this.cleanupInterval = setInterval(() => {
|
|
222
|
+
this.performCleanup();
|
|
223
|
+
}, 60000); // Run cleanup every minute
|
|
224
|
+
this.cleanupInterval.unref();
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* Perform periodic cleanup of idle instances
|
|
229
|
+
*/
|
|
230
|
+
private async performCleanup(): Promise<void> {
|
|
231
|
+
const now = Date.now();
|
|
232
|
+
const instancesToRemove: string[] = [];
|
|
233
|
+
|
|
234
|
+
for (const [id, pooled] of this.instances) {
|
|
235
|
+
const idleTime = now - pooled.lastUsed.getTime();
|
|
236
|
+
const isExpired = idleTime > this.config.maxIdleTime;
|
|
237
|
+
const isOld = (now - pooled.createdAt.getTime()) > this.config.instanceTimeout;
|
|
238
|
+
|
|
239
|
+
if (!pooled.isActive && (isExpired || isOld)) {
|
|
240
|
+
instancesToRemove.push(id);
|
|
241
|
+
}
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
for (const id of instancesToRemove) {
|
|
245
|
+
await this.removeInstance(id);
|
|
246
|
+
logger.debug(`Cleaned up idle instance ${id}`);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (instancesToRemove.length > 0) {
|
|
250
|
+
logger.info(`Cleanup completed: removed ${instancesToRemove.length} idle instances`);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* Get count of active instances
|
|
256
|
+
*/
|
|
257
|
+
private getActiveCount(): number {
|
|
258
|
+
return Array.from(this.instances.values()).filter(p => p.isActive).length;
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Get pool statistics
|
|
263
|
+
*/
|
|
264
|
+
getStats(): {
|
|
265
|
+
totalInstances: number;
|
|
266
|
+
activeInstances: number;
|
|
267
|
+
averageUsage: number;
|
|
268
|
+
oldestInstanceAge: number;
|
|
269
|
+
memoryEstimate: number; // MB
|
|
270
|
+
} {
|
|
271
|
+
const instances = Array.from(this.instances.values());
|
|
272
|
+
const now = Date.now();
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
totalInstances: instances.length,
|
|
276
|
+
activeInstances: this.getActiveCount(),
|
|
277
|
+
averageUsage: instances.reduce((sum, p) => sum + p.usageCount, 0) / instances.length || 0,
|
|
278
|
+
oldestInstanceAge: instances.length > 0
|
|
279
|
+
? Math.max(...instances.map(p => now - p.createdAt.getTime())) / 1000
|
|
280
|
+
: 0,
|
|
281
|
+
memoryEstimate: instances.length * 75 // Approximate 75MB per instance
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Cleanup all instances and stop timers
|
|
287
|
+
*/
|
|
288
|
+
async cleanup(): Promise<void> {
|
|
289
|
+
if (this.cleanupInterval) {
|
|
290
|
+
clearInterval(this.cleanupInterval);
|
|
291
|
+
this.cleanupInterval = undefined;
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
const cleanupPromises = Array.from(this.instances.keys()).map(id =>
|
|
295
|
+
this.removeInstance(id)
|
|
296
|
+
);
|
|
297
|
+
|
|
298
|
+
await Promise.all(cleanupPromises);
|
|
299
|
+
|
|
300
|
+
logger.info('Instance pool cleaned up completely');
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Warm up the pool by pre-creating instances
|
|
305
|
+
*/
|
|
306
|
+
async warmUp(count: number = 1): Promise<void> {
|
|
307
|
+
const warmUpPromises: Promise<void>[] = [];
|
|
308
|
+
|
|
309
|
+
for (let i = 0; i < count && this.instances.size < this.config.maxInstances; i++) {
|
|
310
|
+
warmUpPromises.push(
|
|
311
|
+
this.createNewInstance().then(instance =>
|
|
312
|
+
this.releaseInstance(instance)
|
|
313
|
+
)
|
|
314
|
+
);
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
await Promise.all(warmUpPromises);
|
|
318
|
+
logger.info(`Pool warmed up with ${count} pre-created instances`);
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
// Global optimized pool instance
|
|
323
|
+
export const optimizedInstancePool = OptimizedInstancePool.getInstance();
|